diff --git a/.envrc b/.envrc index f5f9990..bb5cd01 100644 --- a/.envrc +++ b/.envrc @@ -16,7 +16,7 @@ PACKAGES=( make # Deps with makefiles cmake # Deps with cmake doctest # Testing - gcc-toolchain@11 # Compilation + gcc-toolchain@10 # Compilation pkg-config # Deps configuration and configuration of deps deps patchelf # Fix linkage (guix) bear # Generate compile_commands.json for language servers diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..11bf075 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Github language display +*.h linguist-language=C +*.inc linguist-language=C diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ea198d1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: c +script: make +compiler: + - gcc diff --git a/Makefile b/Makefile index 59e6ea0..babea2b 100644 --- a/Makefile +++ b/Makefile @@ -38,11 +38,44 @@ LDLIBS=-lstrophe \ PREFIX ?= /usr/local LIBDIR ?= $(PREFIX)/lib -HDRS=plugin.hh -SRCS=plugin.cpp -DEPS=deps/diff/libdiff.a +HDRS=plugin.hh \ + plugin.h \ + account.h \ + buffer.h \ + channel.h \ + command.h \ + completion.h \ + config.h \ + connection.h \ + input.h \ + message.h \ + omemo.h \ + pgp.h \ + user.h \ + util.h \ + xmpp/stanza.h \ + +SRCS=plugin.cpp \ + account.c \ + buffer.c \ + channel.c \ + command.c \ + completion.c \ + config.c \ + connection.c \ + input.c \ + message.c \ + omemo.c \ + pgp.c \ + user.c \ + util.c \ + xmpp/presence.c \ + xmpp/iq.c \ + +DEPS=deps/diff/libdiff.a \ + TSTS=$(patsubst %.cpp,tests/%.cc,$(SRCS)) tests/main.cc -OBJS=$(patsubst %.cpp,.%.o,$(SRCS)) +OBJS=$(patsubst %.cpp,.%.o,$(patsubst %.c,.%.o,$(patsubst xmpp/%.c,xmpp/.%.o,$(SRCS)))) JOBS=$(patsubst tests/%.cc,tests/.%.o,$(TSTS)) all: weechat-xmpp @@ -57,6 +90,12 @@ xmpp.so: $(OBJS) $(DEPS) $(HDRS) .%.o: %.cpp @$(CXX) $(CPPFLAGS) -c $< -o $@ +.%.o: %.c + @$(CC) $(CFLAGS) -c $< -o $@ + +xmpp/.%.o: xmpp/%.c + @$(CC) $(CFLAGS) -c $< -o $@ + tests/.%.o: tests/%.cc @$(CXX) $(CPPFLAGS) -c $< -o $@ diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..45daedf --- /dev/null +++ b/NOTES.md @@ -0,0 +1,145 @@ +βœ—: No support +β§—: Planned support +βœ“: Partial support +?: Supported, but status not specified +βœ“: Complete support +πŸ•‡: Removed support +☠: Client will never support this XEP ++-------------------------------------------------------------------------+------------------------+---------------+------+-------+----------+-------+--------+-------+ +| XEP | Bruno the Jabberβ„’ Bear | Conversations | Dino | Gajim | Monal IM | Movim | Poezio | yaxim | ++-------------------------------------------------------------------------+------------------------+---------------+------+-------+----------+-------+--------+-------+ +| 0004: Data Forms(Final) | βœ— | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | +| 0012: Last Activity(Final) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ“ | βœ— | +| 0027: Current Jabber OpenPGP Usage(Obsolete) | βœ— | βœ“ | βœ“ | βœ“ | ☠ | βœ— | βœ“ | βœ— | +| 0030: Service Discovery(Final) | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0033: Extended Stanza Addressing(Draft) | βœ— | βœ— | βœ— | βœ“ | ☠ | βœ— | βœ— | βœ— | +| 0045: Multi-User Chat(Draft) | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0047: In-Band Bytestreams(Final) | βœ— | βœ— | ? | βœ“ | ☠ | βœ— | βœ— | βœ— | +| 0048: Bookmarks(Deprecated) | βœ“ | βœ“ | ? | βœ“ | β§— | βœ“ | βœ“ | βœ“ | +| 0049: Private XML Storage(Active) | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | βœ“ | βœ“ | βœ“ | +| 0050: Ad-Hoc Commands(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ“ | βœ“ | βœ— | +| 0054: vcard-temp(Active) | βœ— | βœ“ | βœ“ | βœ“ | ☠ | βœ“ | βœ“ | βœ— | +| 0055: Jabber Search(Active) | βœ— | βœ— | βœ— | βœ“ | ☠ | βœ— | βœ— | βœ— | +| 0059: Result Set Management(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ“ | βœ“ | βœ— | βœ— | +| 0060: Publish-Subscribe(Draft) | βœ— | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | +| 0065: SOCKS5 Bytestreams(Draft) | βœ— | βœ— | βœ— | βœ“ | ☠ | βœ— | βœ— | βœ— | +| 0066: Out of Band Data(Draft) | βœ“ | βœ— | βœ“ | βœ“ | βœ“ | βœ— | βœ— | βœ“ | +| 0070: Verifying HTTP Requests via XMPP(Draft) | βœ— | βœ— | βœ— | βœ“ | ☠ | βœ“ | βœ“ | βœ— | +| 0071: XHTML-IM(Deprecated) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ“ | βœ“ | βœ— | +| 0077: In-Band Registration(Final) | βœ“ | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0080: User Location(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0082: XMPP Date and Time Profiles(Active) | βœ— | βœ— | βœ“ | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0083: Nested Roster Groups(Active) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0084: User Avatar(Draft) | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | +| 0085: Chat State Notifications(Final) | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | +| 0091: Legacy Delayed Delivery(Obsolete) | βœ“ | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ“ | +| 0092: Software Version(Draft) | βœ“ | βœ“ | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0095: Stream Initiation(Deprecated) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0100: Gateway Interaction(Active) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | +| 0106: JID Escaping(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0107: User Mood(Draft) | βœ— | βœ— | βœ— | βœ“ | ☠ | βœ“ | βœ“ | βœ— | +| 0108: User Activity(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ“ | βœ“ | βœ— | +| 0115: Entity Capabilities(Draft) | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0118: User Tune(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ“ | βœ“ | βœ— | +| 0144: Roster Item Exchange(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0145: Annotations(Active) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0146: Remote Controlling Clients(Obsolete) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0147: XMPP URI Scheme Query Components(Active) | βœ“ | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ“ | +| 0153: vCard-Based Avatars(Active) | βœ— | βœ“ | ? | βœ“ | ☠ | βœ“ | βœ“ | βœ— | +| 0156: Discovering Alternative XMPP Connection Methods(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0157: Contact Addresses for XMPP Services(Active) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ“ | βœ“ | βœ— | +| 0158: CAPTCHA Forms(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ“ | βœ— | βœ— | βœ— | +| 0162: Best Practices for Roster and Subscription Management(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | +| 0163: Personal Eventing Protocol(Draft) | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | +| 0166: Jingle(Draft) | βœ— | βœ“ | ? | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0167: Jingle RTP Sessions(Draft) | βœ— | βœ“ | ? | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0172: User Nickname(Draft) | βœ“ | βœ“ | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0174: Serverless Messaging(Final) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0175: Best Practices for Use of SASL ANONYMOUS(Active) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ“ | βœ— | +| 0176: Jingle ICE-UDP Transport Method(Draft) | βœ— | βœ“ | ? | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0178: Best Practices for Use of SASL EXTERNAL with Certificates(Active) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ“ | βœ— | +| 0184: Message Delivery Receipts(Draft) | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0191: Blocking Command(Draft) | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | βœ“ | βœ— | +| 0196: User Gaming(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | +| 0198: Stream Management(Draft) | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | βœ“ | βœ“ | +| 0199: XMPP Ping(Final) | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | βœ“ | βœ“ | βœ“ | +| 0200: Stanza Encryption(Deferred) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0202: Entity Time(Final) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ“ | βœ— | +| 0203: Delayed Delivery(Final) | βœ“ | βœ— | βœ“ | βœ“ | βœ— | βœ“ | βœ“ | βœ“ | +| 0209: Metacontacts(Deferred) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0215: External Service Discovery(Deferred) | βœ— | βœ“ | ? | βœ— | β§— | βœ“ | βœ— | βœ— | +| 0221: Data Forms Media Element(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0222: Persistent Storage of Public Data via PubSub(Active) | βœ— | βœ— | ? | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0223: Persistent Storage of Private Data via PubSub(Active) | βœ— | βœ“ | βœ— | βœ“ | βœ“ | βœ— | βœ— | βœ— | +| 0224: Attention(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ“ | βœ— | +| 0231: Bits of Binary(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ“ | βœ“ | βœ— | +| 0234: Jingle File Transfer(Deferred) | βœ— | βœ“ | ? | βœ“ | ☠ | βœ— | βœ— | βœ— | +| 0237: Roster Versioning(Obsolete) | βœ— | βœ“ | βœ— | βœ“ | βœ“ | βœ— | βœ— | βœ— | +| 0245: The /me Command(Active) | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0249: Direct MUC Invitations(Draft) | βœ“ | βœ“ | ? | βœ“ | β§— | βœ— | βœ“ | βœ“ | +| 0256: Last Activity in Presence(Draft) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | +| 0257: Client Certificate Management for SASL EXTERNAL(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | +| 0258: Security Labels in XMPP(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0260: Jingle SOCKS5 Bytestreams Transport Method(Draft) | βœ— | βœ“ | ? | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0261: Jingle In-Band Bytestreams Transport Method(Draft) | βœ— | βœ“ | βœ“ | βœ“ | ☠ | βœ— | βœ— | βœ— | +| 0277: Microblogging over XMPP(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | +| 0280: Message Carbons(Experimental) | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0284: Shared XML Editing(Deferred) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0286: Mobile Considerations on LTE Networks(Active) | βœ“ | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | +| 0292: vCard4 Over XMPP(Deferred) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ“ | βœ— | βœ— | +| 0293: Jingle RTP Feedback Negotiation(Draft) | βœ— | βœ“ | ? | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0294: Jingle RTP Header Extensions Negotiation(Draft) | βœ— | βœ“ | ? | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0296: Best Practices for Resource Locking(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | +| 0297: Stanza Forwarding(Draft) | βœ“ | βœ— | βœ— | βœ“ | βœ— | βœ“ | βœ“ | βœ“ | +| 0300: Use of Cryptographic Hash Functions in XMPP(Draft) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0306: Extensible Status Conditions for Multi-User Chat(Deferred) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0308: Last Message Correction(Draft) | βœ“ | βœ“ | ? | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0313: Message Archive Management(Experimental) | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0319: Last User Interaction in Presence(Draft) | βœ— | βœ“ | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | +| 0320: Use of DTLS-SRTP in Jingle Sessions(Draft) | βœ— | βœ“ | ? | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0330: Pubsub Subscription(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | +| 0333: Chat Markers(Deferred) | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | βœ— | +| 0334: Message Processing Hints(Deferred) | βœ— | βœ— | ? | βœ“ | βœ— | βœ“ | βœ“ | βœ— | +| 0338: Jingle Grouping Framework(Draft) | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0339: Source-Specific Media Attributes in Jingle(Draft) | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0343: Signaling WebRTC datachannels in Jingle(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | +| 0352: Client State Indication(Draft) | βœ“ | βœ“ | βœ— | βœ— | βœ“ | βœ— | βœ“ | βœ“ | +| 0353: Jingle Message Initiation(Deferred) | βœ— | βœ“ | ? | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0357: Push Notifications(Deferred) | βœ“ | βœ“ | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ“ | +| 0359: Unique and Stable Stanza IDs(Deferred) | βœ“ | βœ— | ? | βœ“ | βœ— | βœ“ | βœ— | βœ“ | +| 0363: HTTP File Upload(Draft) | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | +| 0364: Current Off-the-Record Messaging Usage(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | +| 0367: Message Attaching(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | +| 0368: SRV records for XMPP over TLS(Draft) | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | βœ— | +| 0369: Mediated Information eXchange (MIX)(Experimental) | βœ— | βœ— | βœ— | βœ— | β§— | βœ— | βœ— | βœ— | +| 0372: References(Experimental) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | +| 0373: OpenPGP for XMPP(Experimental) | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0374: OpenPGP for XMPP Instant Messaging(Deferred) | βœ— | βœ— | βœ— | βœ— | β§— | βœ— | βœ— | βœ— | +| 0377: Spam Reporting(Experimental) | βœ— | βœ“ | βœ— | βœ“ | ☠ | βœ— | βœ— | βœ— | +| 0378: OTR Discovery(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | +| 0379: Pre-Authenticated Roster Subscription(Deferred) | βœ“ | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ“ | +| 0380: Explicit Message Encryption(Deferred) | βœ— | βœ— | βœ“ | βœ“ | β§— | βœ“ | βœ“ | βœ— | +| 0384: OMEMO Encryption(Experimental) | βœ— | βœ“ | βœ“ | βœ“ | βœ“ | βœ— | βœ“ | βœ— | +| 0385: Stateless Inline Media Sharing (SIMS)(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | +| 0386: Bind 2.0(Deferred) | βœ— | βœ— | βœ— | βœ— | ☠ | βœ— | βœ— | βœ— | +| 0390: Entity Capabilities 2.0(Deferred) | βœ— | βœ— | βœ— | βœ— | ☠ | βœ— | βœ— | βœ— | +| 0391: Jingle Encrypted Transports(Deferred) | βœ— | βœ“ | ? | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0392: Consistent Color Generation(Deferred) | βœ“ | βœ“ | βœ— | βœ“ | β§— | βœ— | βœ“ | βœ“ | +| 0393: Message Styling(Draft) | βœ“ | βœ“ | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | +| 0396: Jingle Encrypted Transports - OMEMO(Deferred) | βœ— | βœ“ | βœ— | βœ— | ☠ | βœ— | βœ— | βœ— | +| 0397: Instant Stream Resumption(Deferred) | βœ— | βœ— | βœ— | βœ— | ☠ | βœ— | βœ— | βœ— | +| 0398: User Avatar to vCard-Based Avatars Conversion(Deferred) | βœ— | βœ“ | βœ“ | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0401: Easy User Onboarding(Deferred) | βœ“ | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | +| 0402: PEP Native Bookmarks(Draft) | βœ— | βœ— | βœ— | βœ— | β§— | βœ“ | βœ— | βœ— | +| 0409: IM Routing-NG(Deferred) | βœ— | βœ— | βœ— | βœ— | ☠ | βœ— | βœ— | βœ— | +| 0410: MUC Self-Ping (Schrâdinger's Chat)(Draft) | βœ“ | βœ“ | ? | βœ— | ☠ | βœ— | βœ— | βœ“ | +| 0411: Bookmarks Conversion(Draft) | βœ— | βœ“ | βœ— | βœ“ | βœ— | βœ— | βœ— | βœ— | +| 0420: Stanza Content Encryption(Experimental) | βœ— | βœ— | βœ— | βœ— | ☠ | βœ— | βœ— | βœ— | +| 0422: Message Fastening(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | +| 0423: XMPP Compliance Suites 2020(Obsolete) | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | βœ— | +| 0424: Message Retraction(Deferred) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ“ | βœ— | βœ— | +| 0441: Message Archive Management Preferences(Experimental) | βœ— | βœ— | βœ— | βœ“ | βœ“ | βœ— | βœ— | βœ— | +| 0447: Stateless file sharing(Experimental) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0449: Stickers(Experimental) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | +| 0454: OMEMO Media sharing(Experimental) | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | βœ— | ++-------------------------------------------------------------------------+------------------------+---------------+------+-------+----------+-------+--------+-------+ diff --git a/README.org b/README.org new file mode 100644 index 0000000..200c7c1 --- /dev/null +++ b/README.org @@ -0,0 +1,177 @@ +#+TITLE: weechat-xmpp +#+AUTHOR: Tony Olagbaiye +#+EMAIL: frony0@gmail.com +#+DATE: 2018-05-09 +#+DESCRIPTION: Weechat plugin for XMPP +#+KEYWORDS: weechat xmpp c api +#+LANGUAGE: en +#+OPTIONS: H:3 num:nil toc:nil \n:nil @:t ::t |:t ^:t -:t f:t *:t <:t +#+OPTIONS: TeX:t LaTeX:nil skip:nil d:nil todo:t pri:t tags:not-in-toc +#+EXPORT_EXCLUDE_TAGS: exclude +#+STARTUP: showall + +[[https://travis-ci.org/bqv/weechat-xmpp][file:https://api.travis-ci.org/bqv/weechat-xmpp.svg?branch=master]] +[[https://coveralls.io/github/bqv/weechat-xmpp?branch=master][file:https://coveralls.io/repos/github/bqv/weechat-xmpp/badge.svg?branch=master]] +[[https://github.com/bqv/weechat-xmpp/issues][file:https://img.shields.io/github/issues/bqv/weechat-xmpp.svg]] +[[https://github.com/bqv/weechat-xmpp/issues?q=is:issue+is:closed][file:https://img.shields.io/github/issues-closed/bqv/weechat-xmpp.svg]] +[[https://github.com/bqv/weechat-xmpp/blob/master/LICENSE][file:https://img.shields.io/github/license/bqv/weechat-xmpp.svg]] +[[https://github.com/bqv/weechat-extras/][file:https://img.shields.io/badge/weechat--extras-xmpp-blue.svg]] +[[https://github.com/bqv/weechat-extras/][file:https://inverse.chat/badge.svg?room=weechat@muc.xa0.uk]] + + | Status: | XMPP for power users and digital masochists | + | Location: | [[http://github.com/bqv/weechat-xmpp]] | + | Version: | 0.1.1 | + | Disclaimer: | I'm lazy and unashamedly clinically insane | + +* Description + + A weechat plugin in C to extend the chat client to + support XMPP and a currently minimal but ideally maximal + set of XEPs. + My priority here is to have an android-available XMPP client + that hides as little as possible from the user. To use this with + android, set up a relay (`/relay`) and see weechat-android. + I'm gonna rewrite this in C++ at some point when I have a feel + for the full behaviour of an average client. + +* Usage + + 1. Start with =/xmpp add=, use =/help xmpp= for instructions. + + 2. Use =/xmpp connect = with the name set at + add-time. + +* Installing + + Place xmpp.so in the appropriate place for weechat plugins. + +* Dependencies + + - libstrophe (dynamic, dependency) + - libxml2 (dynamic, dependency) + - libsignal-protocol-c (dynamic, dependency) + - rnp (dynamic, dependency) + - weechat (>= v3.0) + + .. or just use the guix spec in .envrc + +* Building + + #+begin_src sh + git clone git://github.com/bqv/weechat-xmpp.git + cd weechat-xmpp + git submodule update --init --recursive + make + make install + #+end_src + + Do *NOT* run make install as root, it installs the plugin to your + local weechat plugins directory + +* Development + + I use emacs for development of this, although I am also a fan of vim. + My debug build process involves static analysis with clang and cppcheck, + and dynamic analysis with address-sanitizer and leak-sanitizer. + My debug evaluation process involves gdb/mi run with the arguments + =-ex "handle SIGPIPE nostop noprint pass" --args weechat -a 2>asan.log= + since part of weechat and it's default plugins use SIGPIPE as control. + + I have no real requests for style of pull requests besides a wish that + you keep vaguely to the indentation style I use for this project. + + Happy coding! + +* Tasks + +** DONE [#A] Implement basic functionality (milestone v0.1) + * [X] Connecting + * [X] Pretty-printing stanzas + * [X] Receiveing and formatting PMs to a dedicated buffer + * [X] Responding to PMs and formatting responses + * [X] Opening PMs (/chat) +** TODO [#A] Implement essential functionality (milestone v0.2) + * [X] Opening PMs with initial message + * [-] OOB messages + * [X] Single media on a line + * [ ] Multiple media inline (protocol?) + * [X] Buffer autoswitch on enter/open + * [X] Handle open/enter jids with a resource without breaking + * [X] Allow /close without crashing + * [ ] [#B] Handle wide errors gracefully + * [ ] [#B] Event-driven MUC entrance + * [ ] [#C] XMPP Ping (xep-199) + * [X] [#A] Highlight + * [-] MUCs + * [X] Opening (/enter) + * [ ] [#B] Leave on /close + * [X] Receiving + * [X] Sending + * [X] With /msg + * [-] [#B] Edits + * [X] [#B] Displaying + * [X] [#B] Tagging + * [ ] [#B] Making + * [X] [#C] Diff highlighting + * [ ] [#B] Handle errors gracefully + * [X] [#B] Presence/nicklist + * [X] [#B] Enters + * [X] [#B] Leaves + * [X] [#B] Tracking + * [X] [#B] Set/show topic + * [-] OMEMO (libsignal-protocol-c) + * [-] Presence + * [X] Disco + * [X] Disco response + * [-] Key Generation / storage (lmdb) + * [X] Generation + * [?] Storage + * [-] Announce + * [X] Device ID + * [ ] Bundles + * [ ] Messages + * [ ] [#C] MUC PMs + * [X] [#A] Send typing notifications + * [X] [#A] Recv typing notifications + * [X] [#C] Read receipts + * [X] Chat Markers (XEP-0333) + * [X] Composing + * [X] Paused + * [?] Active + * [ ] Inactive + * [ ] Gone + * [X] Message Delivery (XEP-0184) + * [X] Message Carbons + * [ ] Service Disco + * [X] MAM Fetching + * [-] Bookmarks / Roster + * [X] Autojoin bookmarks + * [ ] Add bookmarks + * [ ] Delete bookmarks + * [ ] Roster + * [ ] OTR (libotr) + * [X] PGP (rnpgp) + * [X] Use keyrings (e.g. exported from gnupg) + * [X] Presence + * [X] Decryption + * [X] Encryption + * [X] Custom set/clear key (/pgp) + * [ ] OOB data and media + * [ ] Room Explorer (https://search.jabber.network/docs/api) +** TODO [#C] Implement completion engine (milestone v0.3) +** TODO [#D] Close all issues (milestone v1.0) + +* Contributing + + *Your contributions are always welcome!* + Please submit a pull request or create an issue + to add a new or missing feature. + +* Testemonials + + "Weechat-Strophe - for the discerning dual IRCer XMPPer" -- [[github.com/janicez][Ellenor et al Bjornsdottir]] + +* License + + weechat-xmpp is licensed under the Mozilla Public + License Version 2.0 available [[https://www.mozilla.org/en-US/MPL/2.0/][here]] and in LICENSE. diff --git a/account.c b/account.c new file mode 100644 index 0000000..d6a449e --- /dev/null +++ b/account.c @@ -0,0 +1,711 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "plugin.h" +#include "config.h" +#include "input.h" +#include "omemo.h" +#include "account.h" +#include "connection.h" +#include "user.h" +#include "channel.h" +#include "buffer.h" + +struct t_account *accounts = NULL; +struct t_account *last_account = NULL; + +char *account_options[ACCOUNT_NUM_OPTIONS][2] = +{ { "jid", "" }, + { "password", "" }, + { "tls", "normal" }, + { "nickname", "" }, + { "autoconnect", "" }, + { "resource", "" }, + { "status", "probably about to segfault" }, + { "pgp_pubring_path", "${weechat_data_dir}/pubring.gpg" }, + { "pgp_secring_path", "${weechat_data_dir}/secring.gpg" }, + { "pgp_keyid", "" }, +}; + +struct t_account *account__search(const char *name) +{ + struct t_account *ptr_account; + + if (!name) + return NULL; + + for (ptr_account = accounts; ptr_account; + ptr_account = ptr_account->next_account) + { + if (strcmp(ptr_account->name, name) == 0) + return ptr_account; + } + + /* account not found */ + return NULL; +} + +struct t_account *account__casesearch (const char *name) +{ + struct t_account *ptr_account; + + if (!name) + return NULL; + + for (ptr_account = accounts; ptr_account; + ptr_account = ptr_account->next_account) + { + if (weechat_strcasecmp (ptr_account->name, name) == 0) + return ptr_account; + } + + /* account not found */ + return NULL; +} + +int account__search_option(const char *option_name) +{ + int i; + + if (!option_name) + return -1; + + for (i = 0; i < ACCOUNT_NUM_OPTIONS; i++) + { + if (weechat_strcasecmp(account_options[i][0], option_name) == 0) + return i; + } + + /* account option not found */ + return -1; +} + +struct t_account_device *account__search_device(struct t_account *account, int id) +{ + struct t_account_device *ptr_device; + + if (!account) + return NULL; + + for (ptr_device = account->devices; ptr_device; + ptr_device = ptr_device->next_device) + { + if (ptr_device->id == id) + return ptr_device; + } + + return NULL; +} + +void account__add_device(struct t_account *account, + struct t_account_device *device) +{ + struct t_account_device *new_device; + + new_device = account__search_device(account, device->id); + if (!new_device) + { + new_device = malloc(sizeof(*new_device)); + new_device->id = device->id; + new_device->name = strdup(device->name); + + new_device->prev_device = account->last_device; + new_device->next_device = NULL; + if (account->last_device) + (account->last_device)->next_device = new_device; + else + account->devices = new_device; + account->last_device = new_device; + } +} + +void account__free_device(struct t_account *account, struct t_account_device *device) +{ + struct t_account_device *new_devices; + + if (!account || !device) + return; + + /* remove device from devices list */ + if (account->last_device == device) + account->last_device = device->prev_device; + if (device->prev_device) + { + (device->prev_device)->next_device = device->next_device; + new_devices = account->devices; + } + else + new_devices = device->next_device; + + if (device->next_device) + (device->next_device)->prev_device = device->prev_device; + + /* free device data */ + if (device->name) + free(device->name); + + free(device); + + account->devices = new_devices; +} + +void account__free_device_all(struct t_account *account) +{ + while (account->devices) + account__free_device(account, account->devices); +} + +struct t_account_mam_query *account__add_mam_query(struct t_account *account, + struct t_channel *channel, + const char *id, + time_t *start, time_t *end) +{ + struct t_account_mam_query *mam_query; + + if (!(mam_query = account__mam_query_search(account, id))) + { + mam_query = malloc(sizeof(struct t_account_mam_query)); + mam_query->id = strdup(id); + mam_query->with = strdup(channel->id); + + mam_query->has_start = start != NULL; + if (mam_query->has_start) + mam_query->start = *start; + mam_query->has_end = end != NULL; + if (mam_query->has_end) + mam_query->end = *end; + + mam_query->prev_mam_query = account->last_mam_query; + mam_query->next_mam_query = NULL; + if (account->last_mam_query) + (account->last_mam_query)->next_mam_query = mam_query; + else + account->mam_queries = mam_query; + account->last_mam_query = mam_query; + } + + return mam_query; +} + +struct t_account_mam_query *account__mam_query_search(struct t_account *account, + const char *id) +{ + struct t_account_mam_query *ptr_mam_query; + + if (!account || !id) + return NULL; + + for (ptr_mam_query = account->mam_queries; ptr_mam_query; + ptr_mam_query = ptr_mam_query->next_mam_query) + { + if (weechat_strcasecmp(ptr_mam_query->id, id) == 0) + return ptr_mam_query; + } + + return NULL; +} + +void account__mam_query_free(struct t_account *account, + struct t_account_mam_query *mam_query) +{ + struct t_account_mam_query *new_mam_queries; + + if (!account || !mam_query) + return; + + /* remove mam_query from mam_queries list */ + if (account->last_mam_query == mam_query) + account->last_mam_query = mam_query->prev_mam_query; + if (mam_query->prev_mam_query) + { + (mam_query->prev_mam_query)->next_mam_query = mam_query->next_mam_query; + new_mam_queries = account->mam_queries; + } + else + new_mam_queries = mam_query->next_mam_query; + + if (mam_query->next_mam_query) + (mam_query->next_mam_query)->prev_mam_query = mam_query->prev_mam_query; + + /* free mam_query data */ + if (mam_query->id) + free(mam_query->id); + if (mam_query->with) + free(mam_query->with); + + free(mam_query); + + account->mam_queries = new_mam_queries; +} + +void account__mam_query_free_all(struct t_account *account) +{ + while (account->mam_queries) + account__mam_query_free(account, account->mam_queries); +} + +void account__log_emit_weechat(void *const userdata, const xmpp_log_level_t level, + const char *const area, const char *const msg) +{ + struct t_account *account = (struct t_account*)userdata; + + static const char *log_level_name[4] = {"debug", "info", "warn", "error"}; + + const char *tags = level > XMPP_LEVEL_DEBUG ? "no_log" : NULL; + + char *xml; + if ((level == XMPP_LEVEL_DEBUG) && ((xml = strchr(msg, '<')) != NULL)) + { + FILE *nullfd = fopen("/dev/null", "w+"); + xmlGenericErrorContext = nullfd; + + const char *header = strndup(msg, xml - msg); + xmlDocPtr doc = xmlRecoverMemory(xml, strlen(xml)); + if (doc == NULL) { + weechat_printf( + account ? account->buffer : NULL, + "xml: Error parsing the xml document"); + fclose(nullfd); + return; + } + xmlNodePtr root = xmlDocGetRootElement(doc); + const char *tag = root ? (const char*)root->name : ""; + const char *colour = weechat_color("blue"); + if (weechat_strcasecmp(tag, "message")) + { + colour = weechat_color("green"); + } + else if (weechat_strcasecmp(tag, "presence")) + { + colour = weechat_color("yellow"); + } + else if (weechat_strcasecmp(tag, "iq")) + { + colour = weechat_color("red"); + } + xmlChar *buf = malloc(strlen(xml) * 2); + if (buf == NULL) { + weechat_printf( + account ? account->buffer : NULL, + "xml: Error allocating the xml buffer"); + fclose(nullfd); + return; + } + int size = -1; + xmlDocDumpFormatMemory(doc, &buf, &size, 1); + if (size <= 0) { + weechat_printf( + account ? account->buffer : NULL, + "xml: Error formatting the xml document"); + fclose(nullfd); + return; + } + char **lines = weechat_string_split((char*)buf, "\r\n", NULL, + 0, 0, &size); + if (lines[size-1][0] == 0) + lines[--size] = 0; + weechat_printf_date_tags( + account ? account->buffer : NULL, + 0, tags, + _("%s%s (%s): %s"), + weechat_prefix("network"), area, + log_level_name[level], header); + for (int i = 1; i < size; i++) + weechat_printf_date_tags( + account ? account->buffer : NULL, + 0, tags, + _("%s%s"), colour, lines[i]); + + weechat_string_free_split(lines); + fclose(nullfd); + } + else + { + weechat_printf_date_tags( + account ? account->buffer : NULL, + 0, tags, + _("%s%s (%s): %s"), + weechat_prefix("network"), area, + log_level_name[level], msg); + } +} + +struct t_account *account__alloc(const char *name) +{ + struct t_account *new_account; + int i, length; + char *option_name; + + if (account__casesearch(name)) + return NULL; + + /* alloc memory for new account */ + new_account = malloc(sizeof(*new_account)); + if (!new_account) + { + weechat_printf(NULL, + _("%s%s: error when allocating new account"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return NULL; + } + + /* add new account to queue */ + new_account->prev_account = last_account; + new_account->next_account = NULL; + if (last_account) + last_account->next_account = new_account; + else + accounts = new_account; + last_account = new_account; + + /* set name */ + new_account->name = strdup(name); + + /* internal vars */ + new_account->reloading_from_config = 0; + + new_account->is_connected = 0; + new_account->disconnected = 0; + + new_account->current_retry = 0; + new_account->reconnect_delay = 0; + new_account->reconnect_start = 0; + + new_account->logger.handler = &account__log_emit_weechat; + new_account->logger.userdata = new_account; + new_account->context = xmpp_ctx_new(NULL, &new_account->logger); + new_account->connection = NULL; + + new_account->buffer = NULL; + new_account->buffer_as_string = NULL; + + new_account->omemo = NULL; + + new_account->devices = NULL; + new_account->last_device = NULL; + new_account->mam_queries = NULL; + new_account->last_mam_query = NULL; + new_account->users = NULL; + new_account->last_user = NULL; + new_account->channels = NULL; + new_account->last_channel = NULL; + + /* create options with null value */ + for (i = 0; i < ACCOUNT_NUM_OPTIONS; i++) + { + new_account->options[i] = NULL; + + length = strlen(new_account->name) + 1 + + strlen(account_options[i][0]) + + 512 + /* inherited option name(xmpp.account_default.xxx) */ + 1; + option_name = malloc(length); + if (option_name) + { + snprintf(option_name, length, "%s.%s << xmpp.account_default.%s", + new_account->name, + account_options[i][0], + account_options[i][0]); + new_account->options[i] = config__account_new_option( + config_file, + config_section_account, + i, + option_name, + account_options[i][1], + account_options[i][1], + 0, + &config__account_check_value_cb, + account_options[i][0], + NULL, + &config__account_change_cb, + account_options[i][0], + NULL); + config__account_change_cb(account_options[i][0], NULL, + new_account->options[i]); + free(option_name); + } + } + + return new_account; +} + +void account__free_data(struct t_account *account) +{ + //int i; + + if (!account) + return; + + /* free linked lists */ + /* + for (i = 0; i < IRC_SERVER_NUM_OUTQUEUES_PRIO; i++) + { + account__outqueue_free_all(account, i); + } + xmpp_redirect_free_all(account); + xmpp_notify_free_all(account); + */ + account__free_device_all(account); + account__mam_query_free_all(account); + channel__free_all(account); + user__free_all(account); + + /* free hashtables */ + /* + weechat_hashtable_free(account->join_manual); + weechat_hashtable_free(account->join_channel_key); + weechat_hashtable_free(account->join_noswitch); + */ + + /* close xmpp context */ + if (account->connection) + xmpp_conn_release(account->connection); + if (account->context) + xmpp_ctx_free(account->context); + + /* free account data */ + //for (i = 0; i < ACCOUNT_NUM_OPTIONS; i++) + //{ + // if (account->options[i]) + // weechat_config_option_free(account->options[i]); + //} + + if (account->name) + free(account->name); + + if (account->buffer_as_string) + free(account->buffer_as_string); + + if (account->omemo) + omemo__free(account->omemo); + + //channel__free_all(account); + //user__free_all(account); +} + +void account__free(struct t_account *account) +{ + struct t_account *new_accounts; + + if (!account) + return; + + /* + * close account buffer (and all channels/privates) + * (only if we are not in a /upgrade, because during upgrade we want to + * keep connections and closing account buffer would disconnect from account) + */ + if (account->buffer) + weechat_buffer_close(account->buffer); + + /* remove account from queue */ + if (last_account == account) + last_account = account->prev_account; + if (account->prev_account) + { + (account->prev_account)->next_account = account->next_account; + new_accounts = accounts; + } + else + new_accounts = account->next_account; + + if (account->next_account) + (account->next_account)->prev_account = account->prev_account; + + account__free_data(account); + free(account); + accounts = new_accounts; +} + +void account__free_all() +{ + /* for each account in memory, remove it */ + while (accounts) + { + account__free(accounts); + } +} + +void account__disconnect(struct t_account *account, int reconnect) +{ + (void) reconnect; + + struct t_channel *ptr_channel; + + if (account->is_connected) + { + /* + * remove all nicks and write disconnection message on each + * channel/private buffer + */ + user__free_all(account); + weechat_nicklist_remove_all(account->buffer); + for (ptr_channel = account->channels; ptr_channel; + ptr_channel = ptr_channel->next_channel) + { + weechat_nicklist_remove_all(ptr_channel->buffer); + weechat_printf( + ptr_channel->buffer, + _("%s%s: disconnected from account"), + weechat_prefix("network"), WEECHAT_XMPP_PLUGIN_NAME); + } + /* remove away status on account buffer */ + //weechat_buffer_set(account->buffer, "localvar_del_away", ""); + } + + account__close_connection(account); + + if (account->buffer) + { + weechat_printf( + account->buffer, + _("%s%s: disconnected from account"), + weechat_prefix ("network"), WEECHAT_XMPP_PLUGIN_NAME); + } + + if (reconnect) + { + if (account->current_retry++ == 0) + { + account->reconnect_delay = 5; + account->reconnect_start = time(NULL) + account->reconnect_delay; + } + account->current_retry %= 5; + } + else + { + account->current_retry = 0; + account->reconnect_delay = 0; + account->reconnect_start = 0; + } + + /* + account->lag = 0; + account->lag_displayed = -1; + account->lag_check_time.tv_sec = 0; + account->lag_check_time.tv_usec = 0; + account->lag_next_check = time(NULL) + + weechat_config_integer(xmpp_config_network_lag_check); + account->lag_last_refresh = 0; + account__set_lag(account); + */ // lag based on xmpp ping + + account->disconnected = !reconnect; + + /* send signal "account_disconnected" with account name */ + (void) weechat_hook_signal_send("xmpp_account_disconnected", + WEECHAT_HOOK_SIGNAL_STRING, account->name); +} + +void account__disconnect_all() +{ + struct t_account *ptr_account; + + for (ptr_account = accounts; ptr_account; + ptr_account = ptr_account->next_account) + { + account__disconnect(ptr_account, 0); + } +} + +struct t_gui_buffer *account__create_buffer(struct t_account *account) +{ + char buffer_name[256], charset_modifier[256]; + + snprintf(buffer_name, sizeof(buffer_name), + "account.%s", account->name); + account->buffer = weechat_buffer_new(buffer_name, + &input__data_cb, NULL, NULL, + &buffer__close_cb, NULL, NULL); + if (!account->buffer) + return NULL; + + if (!weechat_buffer_get_integer(account->buffer, "short_name_is_set")) + weechat_buffer_set(account->buffer, "short_name", account->name); + weechat_buffer_set(account->buffer, "localvar_set_type", "server"); + weechat_buffer_set(account->buffer, "localvar_set_server", account->name); + weechat_buffer_set(account->buffer, "localvar_set_channel", account->name); + snprintf(charset_modifier, sizeof (charset_modifier), + "account.%s", account->name); + weechat_buffer_set(account->buffer, "localvar_set_charset_modifier", + charset_modifier); + weechat_buffer_set(account->buffer, "title", + (account->name) ? account->name : ""); + + weechat_buffer_set(account->buffer, "nicklist", "1"); + weechat_buffer_set(account->buffer, "nicklist_display_groups", "0"); + weechat_buffer_set_pointer(account->buffer, "nicklist_callback", + &buffer__nickcmp_cb); + weechat_buffer_set_pointer(account->buffer, "nicklist_callback_pointer", + account); + + return account->buffer; +} + +void account__close_connection(struct t_account *account) +{ + if (account->connection) + { + if (xmpp_conn_is_connected(account->connection)) + xmpp_disconnect(account->connection); + } + + account->is_connected = 0; +} + +int account__connect(struct t_account *account) +{ + if (!account->buffer) + { + if (!account__create_buffer(account)) + return 0; + weechat_buffer_set(account->buffer, "display", "auto"); + } + + account__close_connection(account); + + account->is_connected = + connection__connect(account, &account->connection, account_jid(account), + account_password(account), account_tls(account)); + + (void) weechat_hook_signal_send("xmpp_account_connecting", + WEECHAT_HOOK_SIGNAL_STRING, account->name); + + return account->is_connected; +} + +int account__timer_cb(const void *pointer, void *data, int remaining_calls) +{ + (void) pointer; + (void) data; + (void) remaining_calls; + + struct t_account *ptr_account; + + for (ptr_account = accounts; ptr_account; + ptr_account = ptr_account->next_account) + { + if (ptr_account->is_connected + && (xmpp_conn_is_connecting(ptr_account->connection) + || xmpp_conn_is_connected(ptr_account->connection))) + connection__process(ptr_account->context, ptr_account->connection, 10); + else if (ptr_account->disconnected); + else if (ptr_account->reconnect_start > 0 + && ptr_account->reconnect_start < time(NULL)) + { + account__connect(ptr_account); + } + } + + return WEECHAT_RC_OK; +} diff --git a/account.h b/account.h new file mode 100644 index 0000000..4234749 --- /dev/null +++ b/account.h @@ -0,0 +1,154 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _ACCOUNT_H_ +#define _ACCOUNT_H_ + +extern struct t_account *accounts; +extern struct t_account *last_account; + +enum t_account_option +{ + ACCOUNT_OPTION_JID, + ACCOUNT_OPTION_PASSWORD, + ACCOUNT_OPTION_TLS, + ACCOUNT_OPTION_NICKNAME, + ACCOUNT_OPTION_AUTOCONNECT, + ACCOUNT_OPTION_RESOURCE, + ACCOUNT_OPTION_STATUS, + ACCOUNT_OPTION_PGP_PUBRING_PATH, + ACCOUNT_OPTION_PGP_SECRING_PATH, + ACCOUNT_OPTION_PGP_KEYID, + ACCOUNT_NUM_OPTIONS, +}; + +#define account__option_string(account, option) \ + weechat_config_string(account->options[ACCOUNT_OPTION_ ## option]) +#define account__option_integer(account, option) \ + weechat_config_integer(account->options[ACCOUNT_OPTION_ ## option]) +#define account__option_boolean(account, option) \ + weechat_config_boolean(account->options[ACCOUNT_OPTION_ ## option]) +#define account_option_set(account, option, value) \ + weechat_config_option_set(account->options[option], value, 1) + +#define account_jid(account) \ + account->connection && xmpp_conn_is_connected(account->connection) ? \ + xmpp_jid_bare(account->context, xmpp_conn_get_bound_jid(account->connection)) : \ + weechat_config_string(account->options[ACCOUNT_OPTION_JID]) +#define account_jid_device(account) \ + account->connection && xmpp_conn_is_connected(account->connection) ? \ + xmpp_conn_get_bound_jid(account->connection) : \ + xmpp_jid_new(account->context, \ + xmpp_jid_node(account->context, \ + weechat_config_string(account->options[ACCOUNT_OPTION_JID])), \ + xmpp_jid_domain(account->context, \ + weechat_config_string(account->options[ACCOUNT_OPTION_JID])), \ + "weechat") +#define account_password(account) \ + weechat_config_string(account->options[ACCOUNT_OPTION_PASSWORD]) +#define account_tls(account) \ + weechat_config_integer(account->options[ACCOUNT_OPTION_TLS]) +#define account_nickname(account) \ + weechat_config_string(account->options[ACCOUNT_OPTION_NICKNAME]) +#define account_autoconnect(account) \ + weechat_config_boolean(account->options[ACCOUNT_OPTION_AUTOCONNECT]) +#define account_resource(account) \ + weechat_config_string(account->options[ACCOUNT_OPTION_RESOURCE]) +#define account_status(account) \ + weechat_config_string(account->options[ACCOUNT_OPTION_STATUS]) +#define account_pgp_pubring_path(account) \ + weechat_config_string(account->options[ACCOUNT_OPTION_PGP_PUBRING_PATH]) +#define account_pgp_secring_path(account) \ + weechat_config_string(account->options[ACCOUNT_OPTION_PGP_SECRING_PATH]) +#define account_pgp_keyid(account) \ + weechat_config_string(account->options[ACCOUNT_OPTION_PGP_KEYID]) + +struct t_account_device +{ + int id; + char *name; + + struct t_account_device *prev_device; + struct t_account_device *next_device; +}; + +struct t_account_mam_query +{ + char *id; + char *with; + int has_start; + time_t start; + int has_end; + time_t end; + + struct t_account_mam_query *prev_mam_query; + struct t_account_mam_query *next_mam_query; +}; + +struct t_account +{ + char *name; + struct t_config_option *options[ACCOUNT_NUM_OPTIONS]; + + int reloading_from_config; + + int is_connected; + int disconnected; + + int current_retry; + int reconnect_delay; + int reconnect_start; + + xmpp_log_t logger; + xmpp_ctx_t *context; + xmpp_conn_t *connection; + + struct t_gui_buffer *buffer; + char *buffer_as_string; + + struct t_omemo *omemo; + struct t_pgp *pgp; + + struct t_account_device *devices; + struct t_account_device *last_device; + struct t_account_mam_query *mam_queries; + struct t_account_mam_query *last_mam_query; + struct t_user *users; + struct t_user *last_user; + struct t_channel *channels; + struct t_channel *last_channel; + + struct t_account *prev_account; + struct t_account *next_account; +}; + +extern char *account_options[][2]; + +struct t_account *account__search(const char *account_name); +struct t_account *account__casesearch (const char *account_name); +int account__search_option(const char *option_name); +struct t_account_device *account__search_device(struct t_account *account, int id); +void account__add_device(struct t_account *account, struct t_account_device *device); +void account__free_device(struct t_account *account, struct t_account_device *device); +void account__free_device_all(struct t_account *account); +struct t_account_mam_query *account__add_mam_query(struct t_account *account, + struct t_channel *channel, + const char *id, + time_t *start, time_t *end); +struct t_account_mam_query *account__mam_query_search(struct t_account *account, + const char *id); +void account__mam_query_free(struct t_account *account, + struct t_account_mam_query *mam_query); +void account__mam_query_free_all(struct t_account *account); +struct t_account *account__alloc(const char *name); +void account__free_data(struct t_account *account); +void account__free(struct t_account *account); +void account__free_all(); +void account__disconnect(struct t_account *account, int reconnect); +void account__disconnect_all(); +void account__close_connection(struct t_account *account); +int account__connect(struct t_account *account); +int account__timer_cb(const void *pointer, void *data, int remaining_calls); + +#endif /*ACCOUNT_H*/ diff --git a/buffer.c b/buffer.c new file mode 100644 index 0000000..3d5ed3e --- /dev/null +++ b/buffer.c @@ -0,0 +1,191 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include + +#include "plugin.h" +#include "account.h" +#include "channel.h" +#include "buffer.h" + +void buffer__get_account_and_channel(struct t_gui_buffer *buffer, + struct t_account **account, + struct t_channel **channel) +{ + struct t_account *ptr_account; + struct t_channel *ptr_channel; + + if (!buffer) + return; + + *account = NULL; + *channel = NULL; + + /* look for a account or channel using this buffer */ + for (ptr_account = accounts; ptr_account; + ptr_account = ptr_account->next_account) + { + if (ptr_account->buffer == buffer) + { + if (account) + *account = ptr_account; + return; + } + + for (ptr_channel = ptr_account->channels; ptr_channel; + ptr_channel = ptr_channel->next_channel) + { + if (ptr_channel->buffer == buffer) + { + if (account) + *account = ptr_account; + if (channel) + *channel = ptr_channel; + return; + } + } + } +} + +char *buffer__typing_bar_cb(const void *pointer, void *data, + struct t_gui_bar_item *item, + struct t_gui_window *window, + struct t_gui_buffer *buffer, + struct t_hashtable *extra_info) +{ + struct t_channel_typing *ptr_typing; + struct t_account *account; + struct t_channel *channel; + char notification[256]; + unsigned typecount; + + (void) pointer; + (void) data; + (void) item; + (void) window; + (void) extra_info; + + account = NULL; + channel = NULL; + + buffer__get_account_and_channel(buffer, &account, &channel); + + if (!channel) + return strndup("", 0); + + typecount = 0; + + for (ptr_typing = channel->typings; ptr_typing; + ptr_typing = ptr_typing->next_typing) + { + switch (++typecount) + { + case 1: + strcpy(notification, ptr_typing->name); + break; + case 2: + strcat(notification, ", "); + strcat(notification, ptr_typing->name); + break; + case 3: + default: + strcpy(notification, "Several people"); + break; + } + } + + if (typecount) + { + strcat(notification, NG_(" is typing...", + " are typing...", + typecount)); + return strdup(notification); + } + else + { + return strndup("", 0); + } +} + +int buffer__nickcmp_cb(const void *pointer, void *data, + struct t_gui_buffer *buffer, + const char *nick1, + const char *nick2) +{ + struct t_account *account; + + (void) data; + + if (pointer) + account = (struct t_account *)pointer; + else + buffer__get_account_and_channel(buffer, &account, NULL); + + if (account) + { + return weechat_strcasecmp(nick1, nick2); + } + else + { + return weechat_strcasecmp(nick1, nick2); + } +} + +int buffer__close_cb(const void *pointer, void *data, + struct t_gui_buffer *buffer) +{ + struct t_weechat_plugin *buffer_plugin = NULL; + struct t_account *ptr_account = NULL; + struct t_channel *ptr_channel = NULL; + + (void) pointer; + (void) data; + + buffer_plugin = weechat_buffer_get_pointer(buffer, "plugin"); + if (buffer_plugin != weechat_plugin) + return WEECHAT_RC_OK; + buffer__get_account_and_channel(buffer, &ptr_account, &ptr_channel); + + const char* type = weechat_buffer_get_string(buffer, "localvar_type"); + + if (weechat_strcasecmp(type, "server") == 0) + { + if (ptr_account) + { + if (ptr_account->is_connected) + { + account__disconnect(ptr_account, 0); + } + + ptr_account->buffer = NULL; + } + } + else if (weechat_strcasecmp(type, "channel") == 0) + { + if (ptr_account && ptr_channel) + { + if (ptr_account->is_connected) + { + channel__free(ptr_account, ptr_channel); + } + } + } + else if (weechat_strcasecmp(type, "private") == 0) + { + if (ptr_account && ptr_channel) + { + if (ptr_account->is_connected) + { + channel__free(ptr_account, ptr_channel); + } + } + } + else + { + } + + return WEECHAT_RC_OK; +} diff --git a/buffer.h b/buffer.h new file mode 100644 index 0000000..c07794c --- /dev/null +++ b/buffer.h @@ -0,0 +1,26 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_BUFFER_H_ +#define _WEECHAT_XMPP_BUFFER_H_ + +void buffer__get_account_and_channel(struct t_gui_buffer *buffer, + struct t_account **account, + struct t_channel **channel); + +char *buffer__typing_bar_cb(const void *pointer, void *data, + struct t_gui_bar_item *item, + struct t_gui_window *window, + struct t_gui_buffer *buffer, + struct t_hashtable *extra_info); + +int buffer__nickcmp_cb(const void *pointer, void *data, + struct t_gui_buffer *buffer, + const char *nick1, + const char *nick2); + +int buffer__close_cb(const void *pointer, void *data, + struct t_gui_buffer *buffer); + +#endif /*WEECHAT_XMPP_BUFFER_H*/ diff --git a/channel.c b/channel.c new file mode 100644 index 0000000..8577aae --- /dev/null +++ b/channel.c @@ -0,0 +1,1316 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include +#include + +#include "plugin.h" +#include "omemo.h" +#include "account.h" +#include "user.h" +#include "channel.h" +#include "input.h" +#include "buffer.h" +#include "pgp.h" + +const char *channel__transport_name(enum t_channel_transport transport) +{ + switch (transport) + { + case CHANNEL_TRANSPORT_PLAINTEXT: + return "PLAINTEXT"; + case CHANNEL_TRANSPORT_OMEMO: + return "OMEMO"; + case CHANNEL_TRANSPORT_PGP: + return "PGP"; + default: + return NULL; + } +} + +struct t_account *channel__account(struct t_channel *channel) +{ + struct t_account *ptr_account; + struct t_channel *ptr_channel; + + if (!channel) + return NULL; + + for (ptr_account = accounts; ptr_account; + ptr_account = ptr_account->next_account) + { + for (ptr_channel = ptr_account->channels; ptr_channel; + ptr_channel = ptr_channel->next_channel) + { + if (ptr_channel == channel) + return ptr_account; + } + } + + /* account not found */ + return NULL; +} + +struct t_channel *channel__search(struct t_account *account, + const char *id) +{ + struct t_channel *ptr_channel; + + if (!account || !id) + return NULL; + + for (ptr_channel = account->channels; ptr_channel; + ptr_channel = ptr_channel->next_channel) + { + if (weechat_strcasecmp(ptr_channel->id, id) == 0) + return ptr_channel; + } + + return NULL; +} + +struct t_gui_buffer *channel__search_buffer(struct t_account *account, + enum t_channel_type type, + const char *name) +{ + struct t_hdata *hdata_buffer; + struct t_gui_buffer *ptr_buffer; + const char *ptr_type, *ptr_account_name, *ptr_channel_name; + + hdata_buffer = weechat_hdata_get("buffer"); + ptr_buffer = weechat_hdata_get_list(hdata_buffer, "gui_buffers"); + + while (ptr_buffer) + { + if (weechat_buffer_get_pointer(ptr_buffer, "plugin") == weechat_plugin) + { + ptr_type = weechat_buffer_get_string(ptr_buffer, "localvar_type"); + ptr_account_name = weechat_buffer_get_string(ptr_buffer, + "localvar_server"); + ptr_channel_name = weechat_buffer_get_string(ptr_buffer, + "localvar_channel"); + if (ptr_type && ptr_type[0] + && ptr_account_name && ptr_account_name[0] + && ptr_channel_name && ptr_channel_name[0] + && ( (( (type == CHANNEL_TYPE_MUC)) + && (strcmp(ptr_type, "channel") == 0)) + || (( (type == CHANNEL_TYPE_PM)) + && (strcmp(ptr_type, "private") == 0))) + && (strcmp(ptr_account_name, account->name) == 0) + && (weechat_strcasecmp(ptr_channel_name, name) == 0)) + { + return ptr_buffer; + } + } + ptr_buffer = weechat_hdata_move(hdata_buffer, ptr_buffer, 1); + } + + return NULL; +} + +struct t_gui_buffer *channel__create_buffer(struct t_account *account, + enum t_channel_type type, + const char *name) +{ + struct t_gui_buffer *ptr_buffer; + int buffer_created; + const char *short_name, *localvar_channel; + char buffer_name[256]; + + buffer_created = 0; + + snprintf(buffer_name, sizeof(buffer_name), + "xmpp.%s.%s", account->name, name); + + ptr_buffer = channel__search_buffer(account, type, name); + if (ptr_buffer) + { + weechat_nicklist_remove_all(ptr_buffer); + } + else + { + ptr_buffer = weechat_buffer_new(buffer_name, + &input__data_cb, NULL, NULL, + &buffer__close_cb, NULL, NULL); + if (!ptr_buffer) + return NULL; + + buffer_created = 1; + } + + if (buffer_created) + { + if (!weechat_buffer_get_integer(ptr_buffer, "short_name_is_set")) + weechat_buffer_set(ptr_buffer, "short_name", name); + } + else + { + short_name = weechat_buffer_get_string (ptr_buffer, "short_name"); + localvar_channel = weechat_buffer_get_string(ptr_buffer, + "localvar_channel"); + + if (!short_name || + (localvar_channel && (strcmp(localvar_channel, short_name) == 0))) + { + weechat_buffer_set(ptr_buffer, "short_name", + xmpp_jid_node(account->context, name)); + } + } + if(!(account_nickname(account) && strlen(account_nickname(account)))) + account_option_set(account, ACCOUNT_OPTION_NICKNAME, + xmpp_jid_node(account->context, account_jid(account))); + + weechat_buffer_set(ptr_buffer, "name", name); + weechat_buffer_set(ptr_buffer, "localvar_set_type", + (type == CHANNEL_TYPE_PM) ? "private" : "channel"); + weechat_buffer_set(ptr_buffer, "localvar_set_nick", + account_nickname(account)); + weechat_buffer_set(ptr_buffer, "localvar_set_server", account->name); + weechat_buffer_set(ptr_buffer, "localvar_set_channel", name); + weechat_buffer_set(ptr_buffer, "input_multiline", "1"); + + if (buffer_created) + { + (void) weechat_hook_signal_send ("logger_backlog", + WEECHAT_HOOK_SIGNAL_POINTER, + ptr_buffer); + weechat_buffer_set(ptr_buffer, "input_get_unknown_commands", "1"); + if (type != CHANNEL_TYPE_PM) + { + weechat_buffer_set(ptr_buffer, "nicklist", "1"); + weechat_buffer_set(ptr_buffer, "nicklist_display_groups", "0"); + weechat_buffer_set_pointer(ptr_buffer, "nicklist_callback", + &buffer__nickcmp_cb); + weechat_buffer_set_pointer(ptr_buffer, "nicklist_callback_pointer", + account); + } + + weechat_buffer_set(ptr_buffer, "highlight_words_add", + account_nickname(account)); + weechat_buffer_set(ptr_buffer, "highlight_tags_restrict", + "message"); + } + + return ptr_buffer; +} + +void channel__add_nicklist_groups(struct t_account *account, + struct t_channel *channel) +{ + struct t_gui_buffer *ptr_buffer; + char str_group[32]; + + if (channel && channel->type == CHANNEL_TYPE_PM) + return; + + ptr_buffer = channel ? channel->buffer : account->buffer; + + snprintf(str_group, sizeof(str_group), "%03d|%s", 000, "~"); + weechat_nicklist_add_group(ptr_buffer, NULL, str_group, + "weechat.color.nicklist_group", 1); + snprintf(str_group, sizeof(str_group), "%03d|%s", 001, "&"); + weechat_nicklist_add_group(ptr_buffer, NULL, str_group, + "weechat.color.nicklist_group", 1); + snprintf(str_group, sizeof(str_group), "%03d|%s", 002, "@"); + weechat_nicklist_add_group(ptr_buffer, NULL, str_group, + "weechat.color.nicklist_group", 1); + snprintf(str_group, sizeof(str_group), "%03d|%s", 003, "%"); + weechat_nicklist_add_group(ptr_buffer, NULL, str_group, + "weechat.color.nicklist_group", 1); + snprintf(str_group, sizeof(str_group), "%03d|%s", 004, "+"); + weechat_nicklist_add_group(ptr_buffer, NULL, str_group, + "weechat.color.nicklist_group", 1); + snprintf(str_group, sizeof(str_group), "%03d|%s", 005, "?"); + weechat_nicklist_add_group(ptr_buffer, NULL, str_group, + "weechat.color.nicklist_group", 1); + snprintf(str_group, sizeof(str_group), "%03d|%s", 006, "!"); + weechat_nicklist_add_group(ptr_buffer, NULL, str_group, + "weechat.color.nicklist_group", 1); + snprintf(str_group, sizeof(str_group), "%03d|%s", 999, "..."); + weechat_nicklist_add_group(ptr_buffer, NULL, str_group, + "weechat.color.nicklist_group", 1); +} + +struct t_channel *channel__new(struct t_account *account, + enum t_channel_type type, + const char *id, const char *name) +{ + struct t_channel *new_channel, *ptr_channel; + struct t_gui_buffer *ptr_buffer; + struct t_hook *typing_timer, *self_typing_timer; + + if (!account || !id || !name || !name[0]) + return NULL; + + ptr_channel = channel__search(account, id); + if (ptr_channel) + { + return ptr_channel; + } + + ptr_buffer = channel__create_buffer(account, type, name); + if (!ptr_buffer) + return NULL; + + if ((new_channel = malloc(sizeof(*new_channel))) == NULL) + return NULL; + + typing_timer = weechat_hook_timer(1 * 1000, 0, 0, + &channel__typing_cb, + new_channel, NULL); + + self_typing_timer = weechat_hook_timer(1 * 1000, 0, 0, + &channel__self_typing_cb, + new_channel, NULL); + + new_channel->type = type; + new_channel->id = strdup(id); + new_channel->name = strdup(name); + new_channel->transport = CHANNEL_TRANSPORT_PLAINTEXT; + new_channel->pgp_id = NULL; + + new_channel->topic.value = NULL; + new_channel->topic.creator = NULL; + new_channel->topic.last_set = 0; + + new_channel->creator = NULL; + new_channel->last_read = 0.0; + new_channel->unread_count = 0; + new_channel->unread_count_display = 0; + + new_channel->typing_hook_timer = typing_timer; + new_channel->self_typing_hook_timer = self_typing_timer; + new_channel->members_speaking[0] = NULL; + new_channel->members_speaking[1] = NULL; + new_channel->unreads = NULL; + new_channel->self_typings = NULL; + new_channel->last_self_typing = NULL; + new_channel->typings = NULL; + new_channel->last_typing = NULL; + new_channel->members = NULL; + new_channel->last_member = NULL; + new_channel->buffer = ptr_buffer; + new_channel->buffer_as_string = NULL; + + new_channel->prev_channel = account->last_channel; + new_channel->next_channel = NULL; + if (account->last_channel) + (account->last_channel)->next_channel = new_channel; + else + account->channels = new_channel; + account->last_channel = new_channel; + + channel__add_nicklist_groups(account, new_channel); + + if (type != CHANNEL_TYPE_MUC) + { + time_t start = time(NULL); + struct tm *ago = gmtime(&start); + ago->tm_mday -= 7; + start = mktime(ago); + channel__fetch_mam(account, new_channel, NULL, &start, NULL, NULL); + } + + return new_channel; +} + +void channel__member_speaking_add_to_list(struct t_channel *channel, + const char *nick, + int highlight) +{ + int size, to_remove, i; + struct t_weelist_item *ptr_item; + + /* create list if it does not exist */ + if (!channel->members_speaking[highlight]) + channel->members_speaking[highlight] = weechat_list_new(); + + /* remove item if it was already in list */ + ptr_item = weechat_list_casesearch(channel->members_speaking[highlight], nick); + if (ptr_item) + weechat_list_remove(channel->members_speaking[highlight], ptr_item); + + /* add nick in list */ + weechat_list_add(channel->members_speaking[highlight], nick, + WEECHAT_LIST_POS_END, NULL); + + /* reduce list size if it's too big */ + size = weechat_list_size(channel->members_speaking[highlight]); + if (size > CHANNEL_MEMBERS_SPEAKING_LIMIT) + { + to_remove = size - CHANNEL_MEMBERS_SPEAKING_LIMIT; + for (i = 0; i < to_remove; i++) + { + weechat_list_remove( + channel->members_speaking[highlight], + weechat_list_get(channel->members_speaking[highlight], 0)); + } + } +} + +void channel__member_speaking_add(struct t_channel *channel, + const char *nick, int highlight) +{ + if (highlight < 0) + highlight = 0; + if (highlight > 1) + highlight = 1; + if (highlight) + channel__member_speaking_add_to_list(channel, nick, 1); + + channel__member_speaking_add_to_list(channel, nick, 0); +} + +void channel__member_speaking_rename(struct t_channel *channel, + const char *old_nick, + const char *new_nick) +{ + struct t_weelist_item *ptr_item; + int i; + + for (i = 0; i < 2; i++) + { + if (channel->members_speaking[i]) + { + ptr_item = weechat_list_search(channel->members_speaking[i], old_nick); + if (ptr_item) + weechat_list_set(ptr_item, new_nick); + } + } +} + +void channel__member_speaking_rename_if_present(struct t_account *account, + struct t_channel *channel, + const char *nick) +{ + struct t_weelist_item *ptr_item; + int i, j, list_size; + + (void) account; + + for (i = 0; i < 2; i++) + { + if (channel->members_speaking[i]) + { + list_size = weechat_list_size(channel->members_speaking[i]); + for (j = 0; j < list_size; j++) + { + ptr_item = weechat_list_get(channel->members_speaking[i], j); + if (ptr_item && (weechat_strcasecmp(weechat_list_string(ptr_item), + nick) == 0)) + weechat_list_set(ptr_item, nick); + } + } + } +} + +void channel__typing_free(struct t_channel *channel, + struct t_channel_typing *typing) +{ + struct t_channel_typing *new_typings; + + if (!channel || !typing) + return; + + /* remove typing from typings list */ + if (channel->last_typing == typing) + channel->last_typing = typing->prev_typing; + if (typing->prev_typing) + { + (typing->prev_typing)->next_typing = typing->next_typing; + new_typings = channel->typings; + } + else + new_typings = typing->next_typing; + + if (typing->next_typing) + (typing->next_typing)->prev_typing = typing->prev_typing; + + /* free typing data */ + if (typing->id) + free(typing->id); + if (typing->name) + free(typing->name); + + free(typing); + + channel->typings = new_typings; +} + +void channel__typing_free_all(struct t_channel *channel) +{ + while (channel->typings) + channel__typing_free(channel, channel->typings); +} + +int channel__typing_cb(const void *pointer, + void *data, + int remaining_calls) +{ + struct t_channel_typing *ptr_typing, *next_typing; + struct t_channel *channel; + const char *localvar; + unsigned typecount; + time_t now; + + (void) data; + (void) remaining_calls; + + if (!pointer) + return WEECHAT_RC_ERROR; + + channel = (struct t_channel *)pointer; + + now = time(NULL); + + typecount = 0; + + for (ptr_typing = channel->typings; ptr_typing; + ptr_typing = ptr_typing->next_typing) + { + next_typing = ptr_typing->next_typing; + + while (ptr_typing && now - ptr_typing->ts > 5) + { + channel__typing_free(channel, ptr_typing); + ptr_typing = next_typing; + if (ptr_typing) + next_typing = ptr_typing->next_typing; + } + + if (!ptr_typing) + break; + + typecount++; + } + + localvar = weechat_buffer_get_string(channel->buffer, "localvar_typing"); + if (!localvar || strncmp(localvar, typecount > 0 ? "1" : "0", 1) != 0) + weechat_buffer_set(channel->buffer, + "localvar_set_typing", + typecount > 0 ? "1" : "0"); + weechat_bar_item_update("typing"); + + return WEECHAT_RC_OK; +} + +struct t_channel_typing *channel__typing_search(struct t_channel *channel, + const char *id) +{ + struct t_channel_typing *ptr_typing; + + if (!channel || !id) + return NULL; + + for (ptr_typing = channel->typings; ptr_typing; + ptr_typing = ptr_typing->next_typing) + { + if (weechat_strcasecmp(ptr_typing->id, id) == 0) + return ptr_typing; + } + + return NULL; +} + +int channel__add_typing(struct t_channel *channel, + struct t_user *user) +{ + struct t_channel_typing *new_typing; + int ret = 0; + + new_typing = channel__typing_search(channel, user->id); + if (!new_typing) + { + new_typing = malloc(sizeof(*new_typing)); + new_typing->id = strdup(user->id); + new_typing->name = strdup(user->profile.display_name); + + new_typing->prev_typing = channel->last_typing; + new_typing->next_typing = NULL; + if (channel->last_typing) + (channel->last_typing)->next_typing = new_typing; + else + channel->typings = new_typing; + channel->last_typing = new_typing; + + ret = 1; + } + new_typing->ts = time(NULL); + + channel__typing_cb(channel, NULL, 0); + + return ret; +} + +void channel__self_typing_free(struct t_channel *channel, + struct t_channel_typing *typing) +{ + struct t_channel_typing *new_typings; + + if (!channel || !typing) + return; + + /* remove typing from typings list */ + if (channel->last_self_typing == typing) + channel->last_self_typing = typing->prev_typing; + if (typing->prev_typing) + { + (typing->prev_typing)->next_typing = typing->next_typing; + new_typings = channel->self_typings; + } + else + new_typings = typing->next_typing; + + if (typing->next_typing) + (typing->next_typing)->prev_typing = typing->prev_typing; + + /* free typing data */ + if (typing->name) + free(typing->name); + + free(typing); + + channel->self_typings = new_typings; +} + +void channel__self_typing_free_all(struct t_channel *channel) +{ + while (channel->self_typings) + channel__self_typing_free(channel, channel->self_typings); +} + +int channel__self_typing_cb(const void *pointer, + void *data, + int remaining_calls) +{ + struct t_channel_typing *ptr_typing, *next_typing; + struct t_account *account; + struct t_channel *channel; + time_t now; + + (void) data; + (void) remaining_calls; + + if (!pointer) + return WEECHAT_RC_ERROR; + + channel = (struct t_channel *)pointer; + account = channel__account(channel); + + now = time(NULL); + + for (ptr_typing = channel->self_typings; ptr_typing; + ptr_typing = ptr_typing->next_typing) + { + next_typing = ptr_typing->next_typing; + + while (ptr_typing && now - ptr_typing->ts > 10) + { + channel__send_paused(account, channel, ptr_typing->user); + channel__self_typing_free(channel, ptr_typing); + ptr_typing = next_typing; + if (ptr_typing) + next_typing = ptr_typing->next_typing; + } + + if (!ptr_typing) + break; + } + + return WEECHAT_RC_OK; +} + +struct t_channel_typing *channel__self_typing_search(struct t_channel *channel, + struct t_user *user) +{ + struct t_channel_typing *ptr_typing; + + if (!channel) + return NULL; + + for (ptr_typing = channel->self_typings; ptr_typing; + ptr_typing = ptr_typing->next_typing) + { + if (user == ptr_typing->user) + return ptr_typing; + } + + return NULL; +} + +int channel__add_self_typing(struct t_channel *channel, + struct t_user *user) +{ + struct t_channel_typing *new_typing; + int ret = 0; + + new_typing = channel__self_typing_search(channel, user); + if (!new_typing) + { + new_typing = malloc(sizeof(*new_typing)); + new_typing->user = user; + new_typing->name = user ? strdup(user->profile.display_name) : NULL; + + new_typing->prev_typing = channel->last_self_typing; + new_typing->next_typing = NULL; + new_typing->ts = time(NULL); + if (channel->last_self_typing) + (channel->last_self_typing)->next_typing = new_typing; + else + channel->self_typings = new_typing; + channel->last_self_typing = new_typing; + + ret = 1; + } + + channel__self_typing_cb(channel, NULL, 0); + + return ret; +} + +void channel__unread_free(struct t_channel_unread *unread) +{ + if (!unread) + return; + + if (unread->id) + free(unread->id); + if (unread->thread) + free(unread->thread); + free(unread); +} + +void channel__unread_free_all(struct t_channel *channel) +{ + if (channel->unreads) + { + int list_size = weechat_list_size(channel->unreads); + + for (int i = 0; i < list_size; i++) + { + struct t_weelist_item *ptr_item = weechat_list_get(channel->unreads, i); + if (ptr_item) + { + struct t_channel_unread *unread = weechat_list_user_data(ptr_item); + + channel__unread_free(unread); + + weechat_list_remove(channel->unreads, ptr_item); + } + } + + weechat_list_free(channel->unreads); + } +} + +void channel__member_free(struct t_channel *channel, + struct t_channel_member *member) +{ + struct t_channel_member *new_members; + + if (!channel || !member) + return; + + /* remove member from members list */ + if (channel->last_member == member) + channel->last_member = member->prev_member; + if (member->prev_member) + { + (member->prev_member)->next_member = member->next_member; + new_members = channel->members; + } + else + new_members = member->next_member; + + if (member->next_member) + (member->next_member)->prev_member = member->prev_member; + + /* free member data */ + if (member->id) + free(member->id); + if (member->role) + free(member->role); + if (member->affiliation) + free(member->affiliation); + + free(member); + + channel->members = new_members; +} + +void channel__member_free_all(struct t_channel *channel) +{ + while (channel->members) + channel__member_free(channel, channel->members); +} + +void channel__free(struct t_account *account, + struct t_channel *channel) +{ + struct t_channel *new_channels; + + if (!account || !channel) + return; + + /* remove channel from channels list */ + if (account->last_channel == channel) + account->last_channel = channel->prev_channel; + if (channel->prev_channel) + { + (channel->prev_channel)->next_channel = channel->next_channel; + new_channels = account->channels; + } + else + new_channels = channel->next_channel; + + if (channel->next_channel) + (channel->next_channel)->prev_channel = channel->prev_channel; + + /* free hooks */ + if (channel->typing_hook_timer) + weechat_unhook(channel->typing_hook_timer); + if (channel->self_typing_hook_timer) + weechat_unhook(channel->self_typing_hook_timer); + + /* free linked lists */ + channel__self_typing_free_all(channel); + channel__typing_free_all(channel); + channel__member_free_all(channel); + channel__unread_free_all(channel); + + /* free channel data */ + if (channel->id) + free(channel->id); + if (channel->name) + free(channel->name); + if (channel->pgp_id) + free(channel->pgp_id); + if (channel->topic.value) + free(channel->topic.value); + if (channel->topic.creator) + free(channel->topic.creator); + if (channel->creator) + free(channel->creator); + if (channel->members_speaking[0]) + weechat_list_free(channel->members_speaking[0]); + if (channel->members_speaking[1]) + weechat_list_free(channel->members_speaking[1]); + if (channel->buffer_as_string) + free(channel->buffer_as_string); + + free(channel); + + account->channels = new_channels; +} + +void channel__free_all(struct t_account *account) +{ + while (account->channels) + channel__free(account, account->channels); +} + +void channel__update_topic(struct t_channel *channel, + const char* topic, + const char* creator, + int last_set) +{ + if (channel->topic.value) + free(channel->topic.value); + if (channel->topic.creator) + free(channel->topic.creator); + channel->topic.value = (topic) ? strdup(topic) : NULL; + channel->topic.creator = (creator) ? strdup(creator) : NULL; + channel->topic.last_set = last_set; + + if (channel->topic.value) + weechat_buffer_set(channel->buffer, "title", topic); + else + weechat_buffer_set(channel->buffer, "title", ""); +} + +struct t_channel_member *channel__add_member(struct t_account *account, + struct t_channel *channel, + const char *id, const char *client) +{ + struct t_channel_member *member; + struct t_user *user; + + user = user__search(account, id); + + if (user && weechat_strcasecmp(user->id, channel->id) == 0 + && channel->type == CHANNEL_TYPE_MUC) + { + weechat_printf_date_tags(channel->buffer, 0, "log2", "%sMUC: %s", + weechat_prefix("network"), + id); + return NULL; + } + + if (!(member = channel__member_search(channel, id))) + { + member = malloc(sizeof(struct t_channel_member)); + member->id = strdup(id); + + member->role = NULL; + member->affiliation = NULL; + + member->prev_member = channel->last_member; + member->next_member = NULL; + if (channel->last_member) + (channel->last_member)->next_member = member; + else + channel->members = member; + channel->last_member = member; + } + else if (user) + user__nicklist_remove(account, channel, user); + + if (user) + user__nicklist_add(account, channel, user); + + char *jid_bare = xmpp_jid_bare(account->context, user->id); + char *jid_resource = xmpp_jid_resource(account->context, user->id); + if (weechat_strcasecmp(jid_bare, channel->id) == 0 + && channel->type == CHANNEL_TYPE_MUC) + weechat_printf_date_tags(channel->buffer, 0, "xmpp_presence,enter,log4", "%s%s%s%s%s %s%s%s%s %s%s%s%s%s%s%s%s%s%s%s%s%s%s", + weechat_prefix("join"), + user__as_prefix_raw(account, jid_resource), + client ? " (" : "", + client ? client : "", + client ? ")" : "", + user->profile.status ? "is " : "", + weechat_color("irc.color.message_join"), + user->profile.status ? user->profile.status : (user->profile.idle ? "idle" : "entered"), + weechat_color("reset"), + channel->id, + user->profile.status_text ? " [" : "", + user->profile.status_text ? user->profile.status_text : "", + user->profile.status_text ? "]" : "", + weechat_color("yellow"), " as ", weechat_color("reset"), + user->profile.affiliation ? user->profile.affiliation : "", + user->profile.affiliation ? " " : "", + user->profile.role, + user->profile.pgp_id ? weechat_color("gray") : "", + user->profile.pgp_id ? " with PGP:" : "", + user->profile.pgp_id ? user->profile.pgp_id : "", + user->profile.pgp_id ? weechat_color("reset") : ""); + else + weechat_printf_date_tags(channel->buffer, 0, "xmpp_presence,enter,log4", "%s%s (%s) %s%s%s%s %s%s%s%s%s%s%s%s%s", + weechat_prefix("join"), + jid_resource ? user__as_prefix_raw(account, jid_bare) : "You", + jid_resource ? jid_resource : user__as_prefix_raw(account, jid_bare), + user->profile.status ? "is " : "", + weechat_color("irc.color.message_join"), + user->profile.status ? user->profile.status : (user->profile.idle ? "idle" : "entered"), + weechat_color("reset"), + user->profile.idle ? "since " : "", + user->profile.idle ? user->profile.idle : "", + user->profile.status_text ? " [" : "", + user->profile.status_text ? user->profile.status_text : "", + user->profile.status_text ? "]" : "", + user->profile.pgp_id ? weechat_color("gray") : "", + user->profile.pgp_id ? " with PGP:" : "", + user->profile.pgp_id ? user->profile.pgp_id : "", + user->profile.pgp_id ? weechat_color("reset") : ""); + + return member; +} + +struct t_channel_member *channel__member_search(struct t_channel *channel, + const char *id) +{ + struct t_channel_member *ptr_member; + + if (!channel || !id) + return NULL; + + for (ptr_member = channel->members; ptr_member; + ptr_member = ptr_member->next_member) + { + if (weechat_strcasecmp(ptr_member->id, id) == 0) + return ptr_member; + } + + return NULL; +} + +struct t_channel_member *channel__remove_member(struct t_account *account, + struct t_channel *channel, + const char *id, const char *reason) +{ + struct t_channel_member *member; + struct t_user *user; + + user = user__search(account, id); + if (user) + user__nicklist_remove(account, channel, user); + + member = channel__member_search(channel, id); + if (member) + channel__member_free(channel, member); + + char *jid_bare = xmpp_jid_bare(account->context, user->id); + char *jid_resource = xmpp_jid_resource(account->context, user->id); + if (weechat_strcasecmp(jid_bare, channel->id) == 0 + && channel->type == CHANNEL_TYPE_MUC) + weechat_printf_date_tags(channel->buffer, 0, "xmpp_presence,leave,log4", "%s%s %sleft%s %s %s%s%s", + weechat_prefix("quit"), + jid_resource, + weechat_color("irc.color.message_quit"), + weechat_color("reset"), + channel->id, + reason ? "[" : "", + reason ? reason : "", + reason ? "]" : ""); + else + weechat_printf_date_tags(channel->buffer, 0, "xmpp_presence,leave,log4", "%s%s (%s) %sleft%s %s %s%s%s", + weechat_prefix("quit"), + xmpp_jid_bare(account->context, user->id), + xmpp_jid_resource(account->context, user->id), + weechat_color("irc.color.message_quit"), + weechat_color("reset"), + channel->id, + reason ? "[" : "", + reason ? reason : "", + reason ? "]" : ""); + + return member; +} + +void channel__send_message(struct t_account *account, struct t_channel *channel, + const char *to, const char *body) +{ + channel__send_reads(account, channel); + + xmpp_stanza_t *message = xmpp_message_new(account->context, + channel->type == CHANNEL_TYPE_MUC + ? "groupchat" : "chat", + to, NULL); + + char *id = xmpp_uuid_gen(account->context); + xmpp_stanza_set_id(message, id); + xmpp_free(account->context, id); + + char *url = strstr(body, "http"); + if (channel->pgp_id) + { + xmpp_stanza_t *message__x = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__x, "x"); + xmpp_stanza_set_ns(message__x, "jabber:x:encrypted"); + + xmpp_stanza_t *message__x__text = xmpp_stanza_new(account->context); + char *ciphertext = pgp__encrypt(channel->buffer, account->pgp, account_pgp_keyid(account), channel->pgp_id, body); + if (ciphertext) + xmpp_stanza_set_text(message__x__text, ciphertext); + free(ciphertext); + + xmpp_stanza_add_child(message__x, message__x__text); + xmpp_stanza_release(message__x__text); + + xmpp_stanza_add_child(message, message__x); + xmpp_stanza_release(message__x); + + xmpp_stanza_t *message__encryption = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__encryption, "encryption"); + xmpp_stanza_set_ns(message__encryption, "urn:xmpp:eme:0"); + xmpp_stanza_set_attribute(message__encryption, "namespace", "jabber:x:encryption"); + + xmpp_stanza_add_child(message, message__encryption); + xmpp_stanza_release(message__encryption); + + xmpp_message_set_body(message, PGP_ADVICE); + + if (ciphertext && channel->transport != CHANNEL_TRANSPORT_PGP) + { + channel->transport = CHANNEL_TRANSPORT_PGP; + weechat_printf_date_tags(channel->buffer, 0, NULL, "%s%sTransport: %s", + weechat_prefix("network"), weechat_color("gray"), + channel__transport_name(channel->transport)); + } + } + else + { + xmpp_message_set_body(message, body); + + if (channel->transport != CHANNEL_TRANSPORT_PLAINTEXT) + { + channel->transport = CHANNEL_TRANSPORT_PLAINTEXT; + weechat_printf_date_tags(channel->buffer, 0, NULL, "%s%sTransport: %s", + weechat_prefix("network"), weechat_color("gray"), + channel__transport_name(channel->transport)); + } + } + + if (url) + { + xmpp_stanza_t *message__x = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__x, "x"); + xmpp_stanza_set_ns(message__x, "jabber:x:oob"); + + xmpp_stanza_t *message__x__url = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__x__url, "url"); + + xmpp_stanza_t *message__x__url__text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(message__x__url__text, url); + xmpp_stanza_add_child(message__x__url, message__x__url__text); + xmpp_stanza_release(message__x__url__text); + + xmpp_stanza_add_child(message__x, message__x__url); + xmpp_stanza_release(message__x__url); + + xmpp_stanza_add_child(message, message__x); + xmpp_stanza_release(message__x); + } + + xmpp_stanza_t *message__active = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__active, "active"); + xmpp_stanza_set_ns(message__active, "http://jabber./org/protocol/chatstates"); + + xmpp_stanza_add_child(message, message__active); + xmpp_stanza_release(message__active); + + xmpp_send(account->connection, message); + xmpp_stanza_release(message); + if (channel->type != CHANNEL_TYPE_MUC) + weechat_printf_date_tags(channel->buffer, 0, + "xmpp_message,message,private,notify_none,self_msg,log1", + "%s\t%s", + user__as_prefix_raw(account, account_jid(account)), + body); +} + +void channel__send_reads(struct t_account *account, struct t_channel *channel) +{ + if (channel && channel->unreads) + { + int list_size = weechat_list_size(channel->unreads); + + for (int i = 0; i < list_size; i++) + { + struct t_weelist_item *ptr_item = weechat_list_get(channel->unreads, 0); + if (ptr_item) + { + const char *unread_id = weechat_list_string(ptr_item); + struct t_channel_unread *unread = weechat_list_user_data(ptr_item); + + xmpp_stanza_t *message = xmpp_message_new(account->context, NULL, + channel->id, NULL); + + xmpp_stanza_t *message__displayed = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__displayed, "displayed"); + xmpp_stanza_set_ns(message__displayed, "urn:xmpp:chat-markers:0"); + xmpp_stanza_set_id(message__displayed, unread->id ? unread->id : unread_id); + if (unread->thread) + { + xmpp_stanza_t *message__thread = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__thread, "thread"); + + xmpp_stanza_t *message__thread__text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(message__thread__text, unread->thread); + xmpp_stanza_add_child(message__thread, message__thread__text); + xmpp_stanza_release(message__thread__text); + + xmpp_stanza_add_child(message, message__thread); + xmpp_stanza_release(message__thread); + } + + xmpp_stanza_add_child(message, message__displayed); + xmpp_stanza_release(message__displayed); + + xmpp_send(account->connection, message); + xmpp_stanza_release(message); + + channel__unread_free(unread); + + weechat_list_remove(channel->unreads, ptr_item); + } + } + } +} + +void channel__send_typing(struct t_account *account, struct t_channel *channel, + struct t_user *user) +{ + if (channel__add_self_typing(channel, user)) + { + xmpp_stanza_t *message = xmpp_message_new(account->context, + channel->type == CHANNEL_TYPE_MUC + ? "groupchat" : "chat", + user ? user->id : channel->id, NULL); + + xmpp_stanza_t *message__composing = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__composing, "composing"); + xmpp_stanza_set_ns(message__composing, "http://jabber.org/protocol/chatstates"); + + xmpp_stanza_add_child(message, message__composing); + xmpp_stanza_release(message__composing); + + xmpp_send(account->connection, message); + xmpp_stanza_release(message); + } +} + +void channel__send_paused(struct t_account *account, struct t_channel *channel, + struct t_user *user) +{ + xmpp_stanza_t *message = xmpp_message_new(account->context, + channel->type == CHANNEL_TYPE_MUC + ? "groupchat" : "chat", + user ? user->id : channel->id, NULL); + + xmpp_stanza_t *message__paused = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__paused, "paused"); + xmpp_stanza_set_ns(message__paused, "http://jabber.org/protocol/chatstates"); + + xmpp_stanza_add_child(message, message__paused); + xmpp_stanza_release(message__paused); + + xmpp_send(account->connection, message); + xmpp_stanza_release(message); +} + +void channel__fetch_mam(struct t_account *account, struct t_channel *channel, + const char *id, time_t *start, time_t *end, const char* after) +{ + xmpp_stanza_t *iq = xmpp_iq_new(account->context, "set", "juliet1"); + xmpp_stanza_set_id(iq, id ? id : xmpp_uuid_gen(account->context)); + + xmpp_stanza_t *query = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(query, "query"); + xmpp_stanza_set_ns(query, "urn:xmpp:mam:2"); + + xmpp_stanza_t *x = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(x, "x"); + xmpp_stanza_set_ns(x, "jabber:x:data"); + xmpp_stanza_set_attribute(x, "type", "result"); + + xmpp_stanza_t *field, *value, *text; + + { + field = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(field, "field"); + xmpp_stanza_set_attribute(field, "var", "FORM_TYPE"); + xmpp_stanza_set_attribute(field, "type", "hidden"); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, "urn:xmpp:mam:2"); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + xmpp_stanza_add_child(x, field); + xmpp_stanza_release(field); + } + + { + field = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(field, "field"); + xmpp_stanza_set_attribute(field, "var", "with"); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, channel->id); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + xmpp_stanza_add_child(x, field); + xmpp_stanza_release(field); + } + + if (start) + { + field = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(field, "field"); + xmpp_stanza_set_attribute(field, "var", "start"); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + char time[256] = {0}; + strftime(time, sizeof(time), "%Y-%m-%dT%H:%M:%SZ", gmtime(start)); + xmpp_stanza_set_text(text, time); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + xmpp_stanza_add_child(x, field); + xmpp_stanza_release(field); + } + + if (end) + { + field = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(field, "field"); + xmpp_stanza_set_attribute(field, "var", "end"); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + char time[256] = {0}; + strftime(time, sizeof(time), "%Y-%m-%dT%H:%M:%SZ", gmtime(end)); + xmpp_stanza_set_text(text, time); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + xmpp_stanza_add_child(x, field); + xmpp_stanza_release(field); + } + + xmpp_stanza_add_child(query, x); + xmpp_stanza_release(x); + + if (after) + { + xmpp_stanza_t *set, *set__after, *set__after__text; + + set = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(set, "set"); + xmpp_stanza_set_ns(set, "http://jabber.org/protocol/rsm"); + + set__after = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(set__after, "after"); + + set__after__text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(set__after__text, after); + xmpp_stanza_add_child(set__after, set__after__text); + xmpp_stanza_release(set__after__text); + + xmpp_stanza_add_child(set, set__after); + xmpp_stanza_release(set__after); + + xmpp_stanza_add_child(query, set); + xmpp_stanza_release(set); + } + else + account__add_mam_query(account, channel, + xmpp_stanza_get_id(iq), start, end); + + xmpp_stanza_add_child(iq, query); + xmpp_stanza_release(query); + + xmpp_send(account->connection, iq); + xmpp_stanza_release(iq); +} diff --git a/channel.h b/channel.h new file mode 100644 index 0000000..37863f1 --- /dev/null +++ b/channel.h @@ -0,0 +1,196 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_CHANNEL_H_ +#define _WEECHAT_XMPP_CHANNEL_H_ + +#define CHANNEL_MEMBERS_SPEAKING_LIMIT 128 + +enum t_channel_type +{ + CHANNEL_TYPE_MUC, + CHANNEL_TYPE_PM, +}; + +enum t_channel_transport +{ + CHANNEL_TRANSPORT_PLAINTEXT, + CHANNEL_TRANSPORT_OMEMO, + CHANNEL_TRANSPORT_PGP, +}; + +struct t_channel_typing +{ + union { + char *id; + struct t_user *user; + }; + char *name; + time_t ts; + + struct t_channel_typing *prev_typing; + struct t_channel_typing *next_typing; +}; + +struct t_channel_member +{ + char *id; + + char *role; + char *affiliation; + + struct t_channel_member *prev_member; + struct t_channel_member *next_member; +}; + +struct t_channel_topic +{ + char *value; + char *creator; + time_t last_set; +}; + +struct t_channel_unread +{ + char *id; + char *thread; +}; + +struct t_channel +{ + enum t_channel_type type; + char *id; + char *name; + enum t_channel_transport transport; + char *pgp_id; + + struct t_channel_topic topic; + + /* mpim */ + char *creator; + double last_read; + int unread_count; + int unread_count_display; + + struct t_hook *typing_hook_timer; + struct t_hook *self_typing_hook_timer; + struct t_weelist *members_speaking[2]; + struct t_weelist *unreads; + struct t_channel_typing *self_typings; + struct t_channel_typing *last_self_typing; + struct t_channel_typing *typings; + struct t_channel_typing *last_typing; + struct t_channel_member *members; + struct t_channel_member *last_member; + struct t_gui_buffer *buffer; + char *buffer_as_string; + + struct t_channel *prev_channel; + struct t_channel *next_channel; +}; + +const char *channel__transport_name(enum t_channel_transport transport); + +struct t_account *channel__account(struct t_channel *channel); + +struct t_channel *channel__search(struct t_account *account, + const char *id); + +void channel__add_nicklist_groups(struct t_account *account, + struct t_channel *channel); + +struct t_channel *channel__new(struct t_account *account, + enum t_channel_type type, + const char *id, const char *name); + +void channel__member_speaking_add(struct t_channel *channel, + const char *nick, int highlight); + +void channel__member_speaking_rename(struct t_channel *channel, + const char *old_nick, + const char *new_nick); + +void channel__member_speaking_rename_if_present(struct t_account *account, + struct t_channel *channel, + const char *nick); + +void channel__typing_free(struct t_channel *channel, + struct t_channel_typing *typing); + +void channel__typing_free_all(struct t_channel *channel); + +int channel__typing_cb(const void *pointer, + void *data, + int remaining_calls); + +struct t_channel_typing *channel__typing_search(struct t_channel *channel, + const char *id); + +int channel__add_typing(struct t_channel *channel, + struct t_user *user); + +void channel__self_typing_free(struct t_channel *channel, + struct t_channel_typing *typing); + +void channel__self_typing_free_all(struct t_channel *channel); + +int channel__self_typing_cb(const void *pointer, + void *data, + int remaining_calls); + +struct t_channel_typing *channel__self_typing_search(struct t_channel *channel, + struct t_user *user); + +int channel__add_self_typing(struct t_channel *channel, + struct t_user *user); + +int channel__hotlist_update_cb(const void *pointer, void *data, + const char *signal, const char *type_data, + void *signal_data); + +void channel__unread_free(struct t_channel_unread *unread); + +void channel__unread_free_all(struct t_channel *channel); + +void channel__free(struct t_account *account, + struct t_channel *channel); + +void channel__free_all(struct t_account *account); + +void channel__update_topic(struct t_channel *channel, + const char* title, + const char* creator, + int last_set); + +void channel__update_purpose(struct t_channel *channel, + const char* purpose, + const char* creator, + int last_set); + +struct t_channel_member *channel__add_member(struct t_account *account, + struct t_channel *channel, + const char *id, const char *client); + +struct t_channel_member *channel__member_search(struct t_channel *channel, + const char *id); + +struct t_channel_member *channel__remove_member(struct t_account *account, + struct t_channel *channel, + const char *id, const char *reason); + +void channel__send_message(struct t_account *account, struct t_channel *channel, + const char *to, const char *body); + +void channel__send_reads(struct t_account *account, struct t_channel *channel); + +void channel__send_typing(struct t_account *account, struct t_channel *channel, + struct t_user *user); + +void channel__send_paused(struct t_account *account, struct t_channel *channel, + struct t_user *user); + +void channel__fetch_mam(struct t_account *account, struct t_channel *channel, + const char *id, time_t *start, time_t *end, const char *after); + +#endif /*WEECHAT_XMPP_CHANNEL_H*/ diff --git a/command.c b/command.c new file mode 100644 index 0000000..57c7019 --- /dev/null +++ b/command.c @@ -0,0 +1,927 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include + +#include "plugin.h" +//#include "oauth.h" +#include "account.h" +#include "user.h" +#include "channel.h" +#include "buffer.h" +#include "message.h" +#include "command.h" + +#define MAM_DEFAULT_DAYS 2 +#define STR(X) #X + +void command__display_account(struct t_account *account) +{ + int num_channels, num_pv; + + if (account->is_connected) + { + num_channels = 0;//xmpp_account_get_channel_count(account); + num_pv = 0;//xmpp_account_get_pv_count(account); + weechat_printf( + NULL, + " %s %s%s%s %s(%s%s%s) [%s%s%s]%s, %d %s, %d pv", + (account->is_connected) ? "*" : " ", + weechat_color("chat_server"), + account->name, + weechat_color("reset"), + weechat_color("chat_delimiters"), + weechat_color("chat_server"), + account_jid(account), + weechat_color("chat_delimiters"), + weechat_color("reset"), + (account->is_connected) ? _("connected") : _("not connected"), + weechat_color("chat_delimiters"), + weechat_color("reset"), + num_channels, + NG_("channel", "channels", num_channels), + num_pv); + } + else + { + weechat_printf( + NULL, + " %s%s%s %s(%s%s%s)%s", + weechat_color("chat_server"), + account->name, + weechat_color("reset"), + weechat_color("chat_delimiters"), + weechat_color("chat_server"), + account_jid(account), + weechat_color("chat_delimiters"), + weechat_color("reset")); + } +} + +void command__account_list(int argc, char **argv) +{ + int i, one_account_found; + struct t_account *ptr_account2; + char *account_name = NULL; + + for (i = 2; i < argc; i++) + { + if (!account_name) + account_name = argv[i]; + } + if (!account_name) + { + if (accounts) + { + weechat_printf(NULL, ""); + weechat_printf(NULL, _("All accounts:")); + for (ptr_account2 = accounts; ptr_account2; + ptr_account2 = ptr_account2->next_account) + { + command__display_account(ptr_account2); + } + } + else + weechat_printf(NULL, _("No account")); + } + else + { + one_account_found = 0; + for (ptr_account2 = accounts; ptr_account2; + ptr_account2 = ptr_account2->next_account) + { + if (weechat_strcasestr(ptr_account2->name, account_name)) + { + if (!one_account_found) + { + weechat_printf(NULL, ""); + weechat_printf(NULL, + _("Servers with \"%s\":"), + account_name); + } + one_account_found = 1; + command__display_account(ptr_account2); + } + } + if (!one_account_found) + weechat_printf(NULL, + _("No account found with \"%s\""), + account_name); + } +} + +void command__add_account(const char *name, const char *jid, const char *password) +{ + struct t_account *account; + + account = account__casesearch(name); + if (account) + { + weechat_printf( + NULL, + _("%s%s: account \"%s\" already exists, can't add it!"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, + name); + return; + } + + account = account__alloc(name); + if (!account) + { + weechat_printf( + NULL, + _("%s%s: unable to add account"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return; + } + + account->name = strdup(name); + if (jid) + account_option_set(account, ACCOUNT_OPTION_JID, strdup(jid)); + if (password) + account_option_set(account, ACCOUNT_OPTION_PASSWORD, strdup(password)); + if (jid) + account_option_set(account, ACCOUNT_OPTION_NICKNAME, + strdup(xmpp_jid_node(account->context, jid))); + + weechat_printf( + NULL, + _("%s: account %s%s%s %s(%s%s%s)%s added"), + WEECHAT_XMPP_PLUGIN_NAME, + weechat_color("chat_server"), + account->name, + weechat_color("reset"), + weechat_color("chat_delimiters"), + weechat_color("chat_server"), + jid ? jid : "???", + weechat_color("chat_delimiters"), + weechat_color("reset")); +} + +void command__account_add(struct t_gui_buffer *buffer, int argc, char **argv) +{ + char *name, *jid = NULL, *password = NULL; + + (void) buffer; + + switch (argc) + { + case 5: + password = argv[4]; + // fall through + case 4: + jid = argv[3]; + // fall through + case 3: + name = argv[2]; + command__add_account(name, jid, password); + break; + default: + weechat_printf(NULL, _("account add: wrong number of arguments")); + break; + } +} + +int command__connect_account(struct t_account *account) +{ + if (!account) + return 0; + + if (account->is_connected) + { + weechat_printf( + NULL, + _("%s%s: already connected to account \"%s\"!"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, + account->name); + } + + account__connect(account); + + return 1; +} + +int command__account_connect(struct t_gui_buffer *buffer, int argc, char **argv) +{ + int i, nb_connect, connect_ok; + struct t_account *ptr_account; + + (void) buffer; + (void) argc; + (void) argv; + + connect_ok = 1; + + nb_connect = 0; + for (i = 2; i < argc; i++) + { + nb_connect++; + ptr_account = account__search(argv[i]); + if (ptr_account) + { + if (!command__connect_account(ptr_account)) + { + connect_ok = 0; + } + } + else + { + weechat_printf( + NULL, + _("%s%s: account not found \"%s\" " + "(add one first with: /account add)"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, + argv[i]); + } + } + + return (connect_ok) ? WEECHAT_RC_OK : WEECHAT_RC_ERROR; +} + +int command__disconnect_account(struct t_account *account) +{ + if (!account) + return 0; + + if (!account->is_connected) + { + weechat_printf( + NULL, + _("%s%s: not connected to account \"%s\"!"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, + account->name); + } + + account__disconnect(account, 0); + + return 1; +} + +int command__account_disconnect(struct t_gui_buffer *buffer, int argc, char **argv) +{ + int i, nb_disconnect, disconnect_ok; + struct t_account *ptr_account; + + (void) argc; + (void) argv; + + disconnect_ok = 1; + + nb_disconnect = 0; + if (argc < 2) + { + struct t_channel *ptr_channel; + + buffer__get_account_and_channel(buffer, &ptr_account, &ptr_channel); + + if (ptr_account) + { + if (!command__disconnect_account(ptr_account)) + { + disconnect_ok = 0; + } + } + } + for (i = 2; i < argc; i++) + { + nb_disconnect++; + ptr_account = account__search(argv[i]); + if (ptr_account) + { + if (!command__disconnect_account(ptr_account)) + { + disconnect_ok = 0; + } + } + else + { + weechat_printf( + NULL, + _("%s%s: account not found \"%s\" "), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, + argv[i]); + } + } + + return (disconnect_ok) ? WEECHAT_RC_OK : WEECHAT_RC_ERROR; +} + +int command__account_reconnect(struct t_gui_buffer *buffer, int argc, char **argv) +{ + command__account_disconnect(buffer, argc, argv); + return command__account_connect(buffer, argc, argv); +} + +void command__account_delete(struct t_gui_buffer *buffer, int argc, char **argv) +{ + (void) buffer; + + struct t_account *account; + char *account_name; + + if (argc < 3) + { + weechat_printf( + NULL, + _("%sToo few arguments for command\"%s %s\" " + "(help on command: /help %s)"), + weechat_prefix("error"), + argv[0], argv[1], argv[0] + 1); + return; + } + + account = account__search(argv[2]); + if (!account) + { + weechat_printf( + NULL, + _("%s%s: account \"%s\" not found for \"%s\" command"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, + argv[2], "xmpp delete"); + return; + } + if (account->is_connected) + { + weechat_printf( + NULL, + _("%s%s: you cannot delete account \"%s\" because you" + "are connected. Try \"/xmpp disconnect %s\" first."), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, + argv[2], argv[2]); + return; + } + + account_name = strdup(account->name); + account__free(account); + weechat_printf( + NULL, + _("%s: account %s%s%s has been deleted"), + WEECHAT_XMPP_PLUGIN_NAME, + weechat_color("chat_server"), + (account_name) ? account_name : "???", + weechat_color("reset")); + if (account_name) + free(account_name); +} + +int command__account(const void *pointer, void *data, + struct t_gui_buffer *buffer, int argc, + char **argv, char **argv_eol) +{ + + (void) pointer; + (void) data; + (void) buffer; + + if (argc <= 1 || weechat_strcasecmp(argv[1], "list") == 0) + { + command__account_list(argc, argv); + return WEECHAT_RC_OK; + } + + if (argc > 1) + { + if (weechat_strcasecmp(argv[1], "add") == 0) + { + command__account_add(buffer, argc, argv); + return WEECHAT_RC_OK; + } + + if (weechat_strcasecmp(argv[1], "connect") == 0) + { + command__account_connect(buffer, argc, argv); + return WEECHAT_RC_OK; + } + + if (weechat_strcasecmp(argv[1], "disconnect") == 0) + { + command__account_disconnect(buffer, argc, argv); + return WEECHAT_RC_OK; + } + + if (weechat_strcasecmp(argv[1], "reconnect") == 0) + { + command__account_reconnect(buffer, argc, argv); + return WEECHAT_RC_OK; + } + + if (weechat_strcasecmp(argv[1], "delete") == 0) + { + command__account_delete(buffer, argc, argv); + return WEECHAT_RC_OK; + } + + WEECHAT_COMMAND_ERROR; + } + + return WEECHAT_RC_OK; +} + +int command__enter(const void *pointer, void *data, + struct t_gui_buffer *buffer, int argc, + char **argv, char **argv_eol) +{ + struct t_account *ptr_account = NULL; + struct t_channel *ptr_channel = NULL; + xmpp_stanza_t *pres; + char *jid, *pres_jid, *text; + + (void) pointer; + (void) data; + (void) argv; + + buffer__get_account_and_channel(buffer, &ptr_account, &ptr_channel); + + if (!ptr_account) + return WEECHAT_RC_ERROR; + + if (!ptr_account->is_connected) + { + weechat_printf(buffer, + _("%s%s: you are not connected to server"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return WEECHAT_RC_OK; + } + + if (argc > 1) + { + int n_jid = 0; + char **jids = weechat_string_split(argv[1], ",", NULL, 0, 0, &n_jid); + for (int i = 0; i < n_jid; i++) + { + jid = xmpp_jid_bare(ptr_account->context, jids[i]); + pres_jid = jids[i]; + + if(!xmpp_jid_resource(ptr_account->context, pres_jid)) + pres_jid = xmpp_jid_new( + ptr_account->context, + xmpp_jid_node(ptr_account->context, jid), + xmpp_jid_domain(ptr_account->context, jid), + account_nickname(ptr_account) + && strlen(account_nickname(ptr_account)) + ? account_nickname(ptr_account) + : xmpp_jid_node(ptr_account->context, + account_jid(ptr_account))); + + ptr_channel = channel__search(ptr_account, jid); + if (!ptr_channel) + ptr_channel = channel__new(ptr_account, CHANNEL_TYPE_MUC, jid, jid); + + pres = xmpp_presence_new(ptr_account->context); + xmpp_stanza_set_to(pres, pres_jid); + xmpp_stanza_set_from(pres, account_jid(ptr_account)); + + xmpp_stanza_t *pres__x = xmpp_stanza_new(ptr_account->context); + xmpp_stanza_set_name(pres__x, "x"); + xmpp_stanza_set_ns(pres__x, "http://jabber.org/protocol/muc"); + xmpp_stanza_add_child(pres, pres__x); + xmpp_stanza_release(pres__x); + + xmpp_send(ptr_account->connection, pres); + xmpp_stanza_release(pres); + + if (argc > 2) + { + text = argv_eol[2]; + + channel__send_message(ptr_account, ptr_channel, jid, text); + } + + char buf[16]; + int num = weechat_buffer_get_integer(ptr_channel->buffer, "number"); + snprintf(buf, sizeof(buf), "/buffer %d", num); + weechat_command(ptr_account->buffer, buf); + } + weechat_string_free_split(jids); + } + else + { + const char *buffer_jid = weechat_buffer_get_string(buffer, "localvar_channel"); + + pres_jid = xmpp_jid_new( + ptr_account->context, + xmpp_jid_node(ptr_account->context, buffer_jid), + xmpp_jid_domain(ptr_account->context, buffer_jid), + weechat_buffer_get_string(buffer, "localvar_nick")); + + ptr_channel = channel__search(ptr_account, buffer_jid); + if (!ptr_channel) + ptr_channel = channel__new(ptr_account, CHANNEL_TYPE_MUC, buffer_jid, buffer_jid); + + pres = xmpp_presence_new(ptr_account->context); + xmpp_stanza_set_to(pres, pres_jid); + xmpp_stanza_set_from(pres, account_jid(ptr_account)); + + xmpp_stanza_t *pres__x = xmpp_stanza_new(ptr_account->context); + xmpp_stanza_set_name(pres__x, "x"); + xmpp_stanza_set_ns(pres__x, "http://jabber.org/protocol/muc"); + xmpp_stanza_add_child(pres, pres__x); + xmpp_stanza_release(pres__x); + + xmpp_send(ptr_account->connection, pres); + xmpp_stanza_release(pres); + } + + return WEECHAT_RC_OK; +} + +int command__open(const void *pointer, void *data, + struct t_gui_buffer *buffer, int argc, + char **argv, char **argv_eol) +{ + struct t_account *ptr_account = NULL; + struct t_channel *ptr_channel = NULL; + xmpp_stanza_t *pres; + char *jid, *text; + + (void) pointer; + (void) data; + (void) argv; + + buffer__get_account_and_channel(buffer, &ptr_account, &ptr_channel); + + if (!ptr_account) + return WEECHAT_RC_ERROR; + + if (!ptr_account->is_connected) + { + weechat_printf(buffer, + _("%s%s: you are not connected to server"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return WEECHAT_RC_OK; + } + + if (argc > 1) + { + int n_jid = 0; + char **jids = weechat_string_split(argv[1], ",", NULL, 0, 0, &n_jid); + for (int i = 0; i < n_jid; i++) + { + jid = xmpp_jid_bare(ptr_account->context, jids[i]); + + pres = xmpp_presence_new(ptr_account->context); + xmpp_stanza_set_to(pres, jid); + xmpp_stanza_set_from(pres, account_jid(ptr_account)); + xmpp_send(ptr_account->connection, pres); + xmpp_stanza_release(pres); + + ptr_channel = channel__search(ptr_account, jid); + if (!ptr_channel) + ptr_channel = channel__new(ptr_account, CHANNEL_TYPE_PM, jid, jid); + + if (argc > 2) + { + text = argv_eol[2]; + + channel__send_message(ptr_account, ptr_channel, jid, text); + } + + char buf[16]; + int num = weechat_buffer_get_integer(ptr_channel->buffer, "number"); + snprintf(buf, sizeof(buf), "/buffer %d", num); + weechat_command(ptr_account->buffer, buf); + } + weechat_string_free_split(jids); + } + + return WEECHAT_RC_OK; +} + +int command__msg(const void *pointer, void *data, + struct t_gui_buffer *buffer, int argc, + char **argv, char **argv_eol) +{ + struct t_account *ptr_account = NULL; + struct t_channel *ptr_channel = NULL; + xmpp_stanza_t *message; + char *text; + + (void) pointer; + (void) data; + (void) argv; + + buffer__get_account_and_channel(buffer, &ptr_account, &ptr_channel); + + if (!ptr_account) + return WEECHAT_RC_ERROR; + + if (!ptr_channel) + { + weechat_printf( + ptr_account->buffer, + _("%s%s: \"%s\" command can not be executed on a account buffer"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, "msg"); + return WEECHAT_RC_OK; + } + + if (!ptr_account->is_connected) + { + weechat_printf(buffer, + _("%s%s: you are not connected to server"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return WEECHAT_RC_OK; + } + + if (argc > 1) + { + text = argv_eol[1]; + + message = xmpp_message_new(ptr_account->context, + ptr_channel->type == CHANNEL_TYPE_MUC ? "groupchat" : "chat", + ptr_channel->name, NULL); + xmpp_message_set_body(message, text); + xmpp_send(ptr_account->connection, message); + xmpp_stanza_release(message); + if (ptr_channel->type != CHANNEL_TYPE_MUC) + weechat_printf_date_tags(ptr_channel->buffer, 0, + "xmpp_message,message,private,notify_none,self_msg,log1", + "%s\t%s", + user__as_prefix_raw(ptr_account, account_jid(ptr_account)), text); + } + + return WEECHAT_RC_OK; +} + +int command__me(const void *pointer, void *data, + struct t_gui_buffer *buffer, int argc, + char **argv, char **argv_eol) +{ + struct t_account *ptr_account = NULL; + struct t_channel *ptr_channel = NULL; + xmpp_stanza_t *message; + char *text; + + (void) pointer; + (void) data; + (void) argv; + + buffer__get_account_and_channel(buffer, &ptr_account, &ptr_channel); + + if (!ptr_account) + return WEECHAT_RC_ERROR; + + if (!ptr_channel) + { + weechat_printf( + ptr_account->buffer, + _("%s%s: \"%s\" command can not be executed on a account buffer"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, "me"); + return WEECHAT_RC_OK; + } + + if (!ptr_account->is_connected) + { + weechat_printf(buffer, + _("%s%s: you are not connected to server"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return WEECHAT_RC_OK; + } + + if (argc > 1) + { + text = argv_eol[0]; + + message = xmpp_message_new(ptr_account->context, + ptr_channel->type == CHANNEL_TYPE_MUC ? "groupchat" : "chat", + ptr_channel->name, NULL); + xmpp_message_set_body(message, text); + xmpp_send(ptr_account->connection, message); + xmpp_stanza_release(message); + if (ptr_channel->type != CHANNEL_TYPE_MUC) + weechat_printf_date_tags(ptr_channel->buffer, 0, + "xmpp_message,message,action,private,notify_none,self_msg,log1", + "%s%s %s", + weechat_prefix("action"), + user__as_prefix_raw(ptr_account, account_jid(ptr_account)), + strlen(text) > strlen("/me ") ? text+4 : ""); + } + + return WEECHAT_RC_OK; +} + +int command__mam(const void *pointer, void *data, + struct t_gui_buffer *buffer, int argc, + char **argv, char **argv_eol) +{ + struct t_account *ptr_account = NULL; + struct t_channel *ptr_channel = NULL; + int days; + + (void) pointer; + (void) data; + (void) argv_eol; + + buffer__get_account_and_channel(buffer, &ptr_account, &ptr_channel); + + if (!ptr_account) + return WEECHAT_RC_ERROR; + + if (!ptr_channel) + { + weechat_printf( + ptr_account->buffer, + _("%s%s: \"%s\" command can not be executed on a account buffer"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, "mam"); + return WEECHAT_RC_OK; + } + + time_t start = time(NULL); + struct tm *ago = gmtime(&start); + if (argc > 1) + { + errno = 0; + days = strtol(argv[1], NULL, 10); + + if (errno == 0) + ago->tm_mday -= days; + else + { + weechat_printf( + ptr_channel->buffer, + _("%s%s: \"%s\" is not a valid number of %s for %s"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, "days", "mam"); + ago->tm_mday -= MAM_DEFAULT_DAYS; + } + } + else + ago->tm_mday -= MAM_DEFAULT_DAYS; + start = mktime(ago); + channel__fetch_mam(ptr_account, ptr_channel, NULL, &start, NULL, NULL); + + return WEECHAT_RC_OK; +} + +int command__pgp(const void *pointer, void *data, + struct t_gui_buffer *buffer, int argc, + char **argv, char **argv_eol) +{ + struct t_account *ptr_account = NULL; + struct t_channel *ptr_channel = NULL; + char *keyid; + + (void) pointer; + (void) data; + (void) argv; + + buffer__get_account_and_channel(buffer, &ptr_account, &ptr_channel); + + if (!ptr_account) + return WEECHAT_RC_ERROR; + + if (!ptr_channel) + { + weechat_printf( + ptr_account->buffer, + _("%s%s: \"%s\" command can not be executed on a account buffer"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, "pgp"); + return WEECHAT_RC_OK; + } + + if (argc > 1) + { + keyid = argv_eol[1]; + + ptr_channel->pgp_id = strdup(keyid); + } + else + { + ptr_channel->pgp_id = NULL; + } + + return WEECHAT_RC_OK; +} + +int command__xml(const void *pointer, void *data, + struct t_gui_buffer *buffer, int argc, + char **argv, char **argv_eol) +{ + struct t_account *ptr_account = NULL; + struct t_channel *ptr_channel = NULL; + xmpp_stanza_t *stanza; + + (void) pointer; + (void) data; + (void) argv; + + buffer__get_account_and_channel(buffer, &ptr_account, &ptr_channel); + + if (!ptr_account->is_connected) + { + weechat_printf(buffer, + _("%s%s: you are not connected to server"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return WEECHAT_RC_OK; + } + + if (argc > 1) + { + stanza = xmpp_stanza_new_from_string(ptr_account->context, + argv_eol[0]); + if (!stanza) + return WEECHAT_RC_ERROR; + + xmpp_send(ptr_account->connection, stanza); + xmpp_stanza_release(stanza); + } + + return WEECHAT_RC_OK; +} + +void command__init() +{ + struct t_hook *hook; + + hook = weechat_hook_command( + "account", + N_("handle xmpp accounts"), + N_("list" + " || add " + " || connect " + " || disconnect " + " || reconnect " + " || delete "), + N_(" list: list accounts\n" + " add: add a xmpp account\n" + " connect: connect to a xmpp account\n" + "disconnect: disconnect from a xmpp account\n" + " reconnect: reconnect an xmpp account\n" + " delete: delete a xmpp account\n"), + "list" + " || add %(xmpp_account)" + " || connect %(xmpp_account)" + " || disconnect %(xmpp_account)" + " || reconnect %(xmpp_account)" + " || delete %(xmpp_account)", + &command__account, NULL, NULL); + if (!hook) + weechat_printf(NULL, "Failed to setup command /account"); + + hook = weechat_hook_command( + "enter", + N_("enter an xmpp multi-user-chat (muc)"), + N_(""), + N_("jid: muc to enter"), + NULL, &command__enter, NULL, NULL); + if (!hook) + weechat_printf(NULL, "Failed to setup command /enter"); + + hook = weechat_hook_command( + "open", + N_("open a direct xmpp chat"), + N_(""), + N_("jid: jid to target"), + NULL, &command__open, NULL, NULL); + if (!hook) + weechat_printf(NULL, "Failed to setup command /open"); + + hook = weechat_hook_command( + "msg", + N_("send a xmpp message to the current buffer"), + N_(""), + N_("message: message to send"), + NULL, &command__msg, NULL, NULL); + if (!hook) + weechat_printf(NULL, "Failed to setup command /msg"); + + hook = weechat_hook_command( + "me", + N_("send a xmpp action to the current buffer"), + N_(""), + N_("message: message to send"), + NULL, &command__me, NULL, NULL); + if (!hook) + weechat_printf(NULL, "Failed to setup command /me"); + + hook = weechat_hook_command( + "mam", + N_("retrieve mam messages for the current channel"), + N_("[days]"), + N_("days: number of days to fetch (default: " STR(MAM_DEFAULT_DAYS) ")"), + NULL, &command__mam, NULL, NULL); + if (!hook) + weechat_printf(NULL, "Failed to setup command /mam"); + + hook = weechat_hook_command( + "pgp", + N_("set the target pgp key for the current channel"), + N_(""), + N_("keyid: recipient keyid"), + NULL, &command__pgp, NULL, NULL); + if (!hook) + weechat_printf(NULL, "Failed to setup command /pgp"); + + hook = weechat_hook_command( + "xml", + N_("send a raw xml stanza"), + N_(""), + N_("stanza: xml to send"), + NULL, &command__xml, NULL, NULL); + if (!hook) + weechat_printf(NULL, "Failed to setup command /xml"); +} diff --git a/command.h b/command.h new file mode 100644 index 0000000..fb87318 --- /dev/null +++ b/command.h @@ -0,0 +1,13 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_COMMAND_H_ +#define _WEECHAT_XMPP_COMMAND_H_ + +int command__enter(const void *pointer, void *data, + struct t_gui_buffer *buffer, int argc, + char **argv, char **argv_eol); +void command__init(); + +#endif /*WEECHAT_XMPP_COMMAND_H*/ diff --git a/completion.c b/completion.c new file mode 100644 index 0000000..9fb976a --- /dev/null +++ b/completion.c @@ -0,0 +1,155 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include + +#include "plugin.h" +#include "config.h" +#include "account.h" +#include "channel.h" +#include "user.h" +#include "buffer.h" +#include "completion.h" + +void completion__channel_nicks_add_speakers(struct t_gui_completion *completion, + struct t_account *account, + struct t_channel *channel, + int highlight) +{ + struct t_user *user; + const char *member; + int list_size, i; + + if (channel->members_speaking[highlight]) + { + list_size = weechat_list_size(channel->members_speaking[highlight]); + for (i = 0; i < list_size; i++) + { + member = weechat_list_string( + weechat_list_get(channel->members_speaking[highlight], i)); + if (member) + { + user = user__search(account, member); + if (user) + weechat_hook_completion_list_add(completion, + user->profile.display_name, + 1, WEECHAT_LIST_POS_BEGINNING); + } + } + } +} + +int completion__channel_nicks_cb(const void *pointer, void *data, + const char *completion_item, + struct t_gui_buffer *buffer, + struct t_gui_completion *completion) +{ + struct t_account *ptr_account; + struct t_channel *ptr_channel; + struct t_channel_member *ptr_member; + struct t_user *ptr_user; + + /* make C compiler happy */ + (void) pointer; + (void) data; + (void) completion_item; + + ptr_account = NULL; + ptr_channel = NULL; + buffer__get_account_and_channel(buffer, &ptr_account, &ptr_channel); + + if (ptr_channel) + { + switch (ptr_channel->type) + { + case CHANNEL_TYPE_MUC: + case CHANNEL_TYPE_PM: + for (ptr_member = ptr_channel->members; ptr_member; + ptr_member = ptr_member->next_member) + { + ptr_user = user__search(ptr_account, ptr_member->id); + if (ptr_user) + weechat_hook_completion_list_add(completion, + ptr_user->profile.display_name, + 1, WEECHAT_LIST_POS_SORT); + } + /* add recent speakers on channel */ + if (weechat_config_integer(config_look_nick_completion_smart) == CONFIG_NICK_COMPLETION_SMART_SPEAKERS) + { + completion__channel_nicks_add_speakers(completion, ptr_account, ptr_channel, 0); + } + /* add members whose make highlights on me recently on this channel */ + if (weechat_config_integer(config_look_nick_completion_smart) == CONFIG_NICK_COMPLETION_SMART_SPEAKERS_HIGHLIGHTS) + { + completion__channel_nicks_add_speakers(completion, ptr_account, ptr_channel, 1); + } + /* add self member at the end */ + weechat_hook_completion_list_add(completion, + ptr_account->name, + 1, WEECHAT_LIST_POS_END); + break; + } + } + + return WEECHAT_RC_OK; +} + +int completion__accounts_cb(const void *pointer, void *data, + const char *completion_item, + struct t_gui_buffer *buffer, + struct t_gui_completion *completion) +{ + struct t_account *ptr_account; + + /* make C compiler happy */ + (void) pointer; + (void) data; + (void) completion_item; + (void) buffer; + + for (ptr_account = accounts; ptr_account; + ptr_account = ptr_account->next_account) + { + weechat_hook_completion_list_add(completion, account_jid(ptr_account), + 0, WEECHAT_LIST_POS_SORT); + } + + return WEECHAT_RC_OK; +} + +void completion__init() +{ + struct t_config_option *option; + const char *default_template; + + + weechat_hook_completion("nick", + N_("nicks of current Slack channel"), + &completion__channel_nicks_cb, + NULL, NULL); + + weechat_hook_completion("account", + N_("xmpp accounts"), + &completion__accounts_cb, + NULL, NULL); + + option = weechat_config_get("weechat.completion.default_template"); + default_template = weechat_config_string(option); + if (!weechat_strcasestr(default_template, "%(account)")) + { + size_t length = snprintf(NULL, 0, "%s|%s", + default_template, + "%(account)") + 1; + char *new_template = malloc(length); + snprintf(new_template, length, "%s|%s", + default_template, + "%(account)"); + weechat_config_option_set(option, new_template, 1); + free(new_template); + } +} diff --git a/completion.h b/completion.h new file mode 100644 index 0000000..da99b23 --- /dev/null +++ b/completion.h @@ -0,0 +1,10 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_COMPLETION_H_ +#define _WEECHAT_XMPP_COMPLETION_H_ + +void completion__init(); + +#endif /*WEECHAT_XMPP_COMPLETION_H*/ diff --git a/config.c b/config.c new file mode 100644 index 0000000..fc89b78 --- /dev/null +++ b/config.c @@ -0,0 +1,498 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include + +#include "plugin.h" +#include "account.h" +#include "config.h" + +struct t_config_file *config_file; + +struct t_config_section *config_section_account_default; +struct t_config_section *config_section_account; + +struct t_config_option *config_look_nick_completion_smart; + +struct t_config_option *config_account_default[ACCOUNT_NUM_OPTIONS]; + +int config__account_check_value_cb(const void *pointer, void *data, + struct t_config_option *option, + const char *value) +{ + (void) pointer; + (void) data; + (void) option; + (void) value; + + return 1; +} + +void config__account_change_cb(const void *pointer, void *data, + struct t_config_option *option) +{ + (void) pointer; + (void) data; + + const char *name = + weechat_config_option_get_string(option, "name"); + const char *value = + weechat_config_option_get_string(option, "value"); + + int split_num; + char **split = weechat_string_split(name, ".", NULL, 0, 2, &split_num); + struct t_account *account = account__search(split[0]); + if (split_num >= 2 && account) + { + const char *key = split[1]; + + (void) key; + (void) value; + } + + weechat_string_free_split(split); +} + +void config__account_default_change_cb(const void *pointer, void *data, + struct t_config_option *option) +{ + (void) pointer; + (void) data; + (void) option; +} + +struct t_config_option * +config__account_new_option (struct t_config_file *config_file, + struct t_config_section *section, + int index_option, + const char *option_name, + const char *default_value, + const char *value, + int null_value_allowed, + int (*callback_check_value)(const void *pointer, + void *data, + struct t_config_option *option, + const char *value), + const void *callback_check_value_pointer, + void *callback_check_value_data, + void (*callback_change)(const void *pointer, + void *data, + struct t_config_option *option), + const void *callback_change_pointer, + void *callback_change_data) +{ + struct t_config_option *new_option; + + new_option = NULL; + + switch (index_option) + { + case ACCOUNT_OPTION_JID: + new_option = weechat_config_new_option ( + config_file, section, + option_name, "string", + N_("XMPP Account JID"), + NULL, 0, 0, + default_value, value, + null_value_allowed, + callback_check_value, + callback_check_value_pointer, + callback_check_value_data, + callback_change, + callback_change_pointer, + callback_change_data, + NULL, NULL, NULL); + break; + case ACCOUNT_OPTION_PASSWORD: + new_option = weechat_config_new_option ( + config_file, section, + option_name, "string", + N_("XMPP Account Password"), + NULL, 0, 0, + default_value, value, + null_value_allowed, + callback_check_value, + callback_check_value_pointer, + callback_check_value_data, + callback_change, + callback_change_pointer, + callback_change_data, + NULL, NULL, NULL); + break; + case ACCOUNT_OPTION_TLS: + new_option = weechat_config_new_option ( + config_file, section, + option_name, "integer", + N_("XMPP Server TLS Policy"), + "disable|normal|trust", 0, 0, + default_value, value, + null_value_allowed, + callback_check_value, + callback_check_value_pointer, + callback_check_value_data, + callback_change, + callback_change_pointer, + callback_change_data, + NULL, NULL, NULL); + break; + case ACCOUNT_OPTION_NICKNAME: + new_option = weechat_config_new_option ( + config_file, section, + option_name, "string", + N_("XMPP Account Nickname"), + NULL, 0, 0, + default_value, value, + null_value_allowed, + callback_check_value, + callback_check_value_pointer, + callback_check_value_data, + callback_change, + callback_change_pointer, + callback_change_data, + NULL, NULL, NULL); + break; + case ACCOUNT_OPTION_AUTOCONNECT: + new_option = weechat_config_new_option ( + config_file, section, + option_name, "boolean", + N_("Autoconnect XMPP Account"), + NULL, 0, 0, + default_value, value, + null_value_allowed, + callback_check_value, + callback_check_value_pointer, + callback_check_value_data, + callback_change, + callback_change_pointer, + callback_change_data, + NULL, NULL, NULL); + break; + case ACCOUNT_OPTION_RESOURCE: + new_option = weechat_config_new_option ( + config_file, section, + option_name, "string", + N_("XMPP Account Resource"), + NULL, 0, 0, + default_value, value, + null_value_allowed, + callback_check_value, + callback_check_value_pointer, + callback_check_value_data, + callback_change, + callback_change_pointer, + callback_change_data, + NULL, NULL, NULL); + break; + case ACCOUNT_OPTION_STATUS: + new_option = weechat_config_new_option ( + config_file, section, + option_name, "string", + N_("XMPP Account Login Status"), + NULL, 0, 0, + default_value, value, + null_value_allowed, + callback_check_value, + callback_check_value_pointer, + callback_check_value_data, + callback_change, + callback_change_pointer, + callback_change_data, + NULL, NULL, NULL); + break; + case ACCOUNT_OPTION_PGP_PUBRING_PATH: + new_option = weechat_config_new_option ( + config_file, section, + option_name, "string", + N_("XMPP Account PGP Public Keyring Path"), + NULL, 0, 0, + default_value, value, + null_value_allowed, + callback_check_value, + callback_check_value_pointer, + callback_check_value_data, + callback_change, + callback_change_pointer, + callback_change_data, + NULL, NULL, NULL); + break; + case ACCOUNT_OPTION_PGP_SECRING_PATH: + new_option = weechat_config_new_option ( + config_file, section, + option_name, "string", + N_("XMPP Account PGP Secure Keyring Path"), + NULL, 0, 0, + default_value, value, + null_value_allowed, + callback_check_value, + callback_check_value_pointer, + callback_check_value_data, + callback_change, + callback_change_pointer, + callback_change_data, + NULL, NULL, NULL); + break; + case ACCOUNT_OPTION_PGP_KEYID: + new_option = weechat_config_new_option ( + config_file, section, + option_name, "string", + N_("XMPP Account PGP Key ID"), + NULL, 0, 0, + default_value, value, + null_value_allowed, + callback_check_value, + callback_check_value_pointer, + callback_check_value_data, + callback_change, + callback_change_pointer, + callback_change_data, + NULL, NULL, NULL); + break; + case ACCOUNT_NUM_OPTIONS: + break; + } + + return new_option; +} + +void config__account_create_default_options(struct t_config_section *section) +{ + int i; + + for (i = 0; i < ACCOUNT_NUM_OPTIONS; i++) + { + config_account_default[i] = config__account_new_option( + config_file, + section, + i, + account_options[i][0], + account_options[i][1], + account_options[i][1], + 0, + &config__account_check_value_cb, + account_options[i][0], + NULL, + &config__account_default_change_cb, + account_options[i][0], + NULL); + } +} + + + +int config__account_read_cb (const void *pointer, void *data, + struct t_config_file *config_file, + struct t_config_section *section, + const char *option_name, const char *value) +{ + struct t_account *ptr_account; + int index_option, rc, i; + char *pos_option, *account_name; + + (void) pointer; + (void) data; + (void) config_file; + (void) section; + + rc = WEECHAT_CONFIG_OPTION_SET_ERROR; + + if (option_name) + { + pos_option = strrchr(option_name, '.'); + if (pos_option) + { + account_name = weechat_strndup(option_name, + pos_option - option_name); + pos_option++; + if (account_name) + { + index_option = account__search_option(pos_option); + if (index_option >= 0) + { + ptr_account = account__search(account_name); + if (!ptr_account) + ptr_account = account__alloc(account_name); + if (ptr_account) + { + if (!ptr_account->reloading_from_config++) + { + for (i = 0; i < ACCOUNT_NUM_OPTIONS; i++) + { + weechat_config_option_set( + ptr_account->options[i], NULL, 1); + } + } + ptr_account->reloading_from_config %= + ACCOUNT_NUM_OPTIONS; + rc = weechat_config_option_set( + ptr_account->options[index_option], value, 1); + if (!ptr_account->reloading_from_config) + { + const char *ac_global = weechat_info_get("auto_connect", NULL); + int ac_local = weechat_config_boolean( + ptr_account->options[ACCOUNT_OPTION_AUTOCONNECT]); + if (ac_local && (strcmp(ac_global, "1") == 0)) + account__connect(ptr_account); + } + } + else + { + weechat_printf( + NULL, + _("%s%s: error adding account \"%s\""), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, + account_name); + } + } + free(account_name); + } + } + } + + if (rc == WEECHAT_CONFIG_OPTION_SET_ERROR) + { + weechat_printf( + NULL, + _("%s%s: error creating account option \"%s\""), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, option_name); + } + + return rc; +} + +int config__account_write_cb(const void *pointer, void *data, + struct t_config_file *config_file, + const char *section_name) +{ + struct t_account *ptr_account; + int i; + + (void) pointer; + (void) data; + + if (!weechat_config_write_line(config_file, section_name, NULL)) + return WEECHAT_CONFIG_WRITE_ERROR; + + for (ptr_account = accounts; ptr_account; + ptr_account = ptr_account->next_account) + { + for (i = 0; i < ACCOUNT_NUM_OPTIONS; i++) + { + if (!weechat_config_write_option(config_file, + ptr_account->options[i])) + return WEECHAT_CONFIG_WRITE_ERROR; + } + } + + return WEECHAT_CONFIG_WRITE_OK; +} + +int config__reload (const void *pointer, void *data, + struct t_config_file *config_file) +{ + (void) pointer; + (void) data; + + weechat_config_section_free_options(config_section_account_default); + weechat_config_section_free_options(config_section_account); + account__free_all(); + + return weechat_config_reload(config_file); +} + +int config__init() +{ + struct t_config_section *ptr_section; + + config_file = weechat_config_new(WEECHAT_XMPP_CONFIG_NAME, + &config__reload, NULL, NULL); + + if(!config_file) + return 0; + + ptr_section = weechat_config_new_section( + config_file, "look", + 0, 0, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL); + + if (!ptr_section) + { + weechat_config_free(config_file); + config_file = NULL; + return 0; + } + + config_look_nick_completion_smart = weechat_config_new_option ( + config_file, ptr_section, + "nick_completion_smart", "integer", + N_("smart completion for nicks (completes first with last speakers): " + "speakers = all speakers (including highlights), " + "speakers_highlights = only speakers with highlight"), + "off|speakers|speakers_highlights", 0, 0, "speakers", NULL, 0, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + + ptr_section = weechat_config_new_section( + config_file, "account_default", + 0, 0, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL); + + if (!ptr_section) + { + weechat_config_free(config_file); + config_file = NULL; + return 0; + } + + config_section_account_default = ptr_section; + + config__account_create_default_options(ptr_section); + + ptr_section = weechat_config_new_section( + config_file, "account", + 0, 0, + &config__account_read_cb, NULL, NULL, + &config__account_write_cb, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL); + + if (!ptr_section) + { + weechat_config_free(config_file); + config_file = NULL; + return 0; + } + + config_section_account = ptr_section; + + return 1; +} + +int config__read() +{ + int rc; + + rc = weechat_config_read(config_file); + + return rc; +} + +int config__write() +{ + return weechat_config_write(config_file); +} + +void config__free() +{ +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..8b2214d --- /dev/null +++ b/config.h @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_CONFIG_H_ +#define _WEECHAT_XMPP_CONFIG_H_ + +#define WEECHAT_XMPP_CONFIG_NAME "xmpp" + +enum t_config_nick_completion +{ + CONFIG_NICK_COMPLETION_SMART_OFF = 0, + CONFIG_NICK_COMPLETION_SMART_SPEAKERS, + CONFIG_NICK_COMPLETION_SMART_SPEAKERS_HIGHLIGHTS, +}; + +extern struct t_config_file *config_file; + +extern struct t_config_section *config_section_account_default; +extern struct t_config_section *config_section_account; + +extern struct t_config_option *config_look_nick_completion_smart; + +extern struct t_config_option *config_account_default[]; + +int config__account_check_value_cb(const void *pointer, void *data, + struct t_config_option *option, + const char *value); + +void config__account_change_cb(const void *pointer, void *data, + struct t_config_option *option); + +struct t_config_option *config__account_new_option (struct t_config_file *config_file, + struct t_config_section *section, + int index_option, + const char *option_name, + const char *default_value, + const char *value, + int null_value_allowed, + int (*callback_check_value)(const void *pointer, + void *data, + struct t_config_option *option, + const char *value), + const void *callback_check_value_pointer, + void *callback_check_value_data, + void (*callback_change)(const void *pointer, + void *data, + struct t_config_option *option), + const void *callback_change_pointer, + void *callback_change_data); + +extern int config__init(); +extern int config__read(); +extern int config__write(); +extern void config__free(); + +#endif /*WEECHAT_XMPP_CONFIG_H*/ diff --git a/connection.c b/connection.c new file mode 100644 index 0000000..c8dc2c0 --- /dev/null +++ b/connection.c @@ -0,0 +1,1281 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "plugin.h" +#include "deps/diff/diff.h" +#include "xmpp/stanza.h" +#include "config.h" +#include "account.h" +#include "user.h" +#include "channel.h" +#include "connection.h" +#include "omemo.h" +#include "pgp.h" +#include "util.h" + +void connection__init() +{ + xmpp_initialize(); +} + +int connection__version_handler(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata) +{ + xmpp_stanza_t *reply, *query, *name, *version, *text; + const char *ns; + struct t_account *account = (struct t_account *)userdata; + const char *weechat_name = "weechat"; + char *weechat_version = weechat_info_get("version", NULL); + + weechat_printf(NULL, "Received version request from %s", xmpp_stanza_get_from(stanza)); + + reply = xmpp_stanza_reply(stanza); + xmpp_stanza_set_type(reply, "result"); + + query = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(query, "query"); + ns = xmpp_stanza_get_ns(xmpp_stanza_get_children(stanza)); + if (ns) { + xmpp_stanza_set_ns(query, ns); + } + + name = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(name, "name"); + xmpp_stanza_add_child(query, name); + xmpp_stanza_release(name); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, weechat_name); + xmpp_stanza_add_child(name, text); + xmpp_stanza_release(text); + + version = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(version, "version"); + xmpp_stanza_add_child(query, version); + xmpp_stanza_release(version); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, weechat_version); + xmpp_stanza_add_child(version, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(reply, query); + xmpp_stanza_release(query); + + xmpp_send(conn, reply); + xmpp_stanza_release(reply); + if (weechat_version) + free(weechat_version); + + return 1; +} + +int connection__presence_handler(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata) +{ + (void) conn; + + struct t_account *account = (struct t_account *)userdata; + struct t_user *user; + struct t_channel *channel; + xmpp_stanza_t *iq__x_signed, *iq__x_muc_user, *show, *idle, *iq__x__item, *iq__c, *iq__status; + const char *from, *from_bare, *from_res, *type, *role = NULL, *affiliation = NULL, *jid = NULL; + const char *show__text = NULL, *idle__since = NULL, *certificate = NULL, *node = NULL, *ver = NULL; + char *clientid = NULL, *status; + + from = xmpp_stanza_get_from(stanza); + if (from == NULL) + return 1; + from_bare = xmpp_jid_bare(account->context, from); + from_res = xmpp_jid_resource(account->context, from); + type = xmpp_stanza_get_type(stanza); + show = xmpp_stanza_get_child_by_name(stanza, "show"); + show__text = show ? xmpp_stanza_get_text(show) : NULL; + idle = xmpp_stanza_get_child_by_name_and_ns(stanza, "idle", "urn:xmpp:idle:1"); + idle__since = idle ? xmpp_stanza_get_attribute(idle, "since") : NULL; + iq__x_signed = xmpp_stanza_get_child_by_name_and_ns( + stanza, "x", "jabber:x:signed"); + if (iq__x_signed) + { + certificate = xmpp_stanza_get_text(iq__x_signed); + } + iq__c = xmpp_stanza_get_child_by_name_and_ns( + stanza, "c", "http://jabber.org/protocol/caps"); + if (iq__c) + { + node = xmpp_stanza_get_attribute(iq__c, "node"); + ver = xmpp_stanza_get_attribute(iq__c, "ver"); + if (node && ver) + { + int len = strlen(node)+1+strlen(ver); + clientid = malloc(sizeof(char)*len); + snprintf(clientid, len, "%s#%s", node, ver); + } + } + iq__status = xmpp_stanza_get_child_by_name(stanza, "status"); + status = iq__status ? xmpp_stanza_get_text(iq__status) : NULL; + iq__x_muc_user = xmpp_stanza_get_child_by_name_and_ns( + stanza, "x", "http://jabber.org/protocol/muc#user"); + + channel = channel__search(account, from_bare); + if (weechat_strcasecmp(type, "unavailable") && !iq__x_muc_user && !channel) + channel = channel__new(account, CHANNEL_TYPE_PM, from_bare, from_bare); + + if (iq__x_muc_user) + for (iq__x__item = xmpp_stanza_get_children(iq__x_muc_user); + iq__x__item; iq__x__item = xmpp_stanza_get_next(iq__x__item)) + { + if (weechat_strcasecmp(xmpp_stanza_get_name(iq__x__item), "item") != 0) + continue; + + role = xmpp_stanza_get_attribute(iq__x__item, "role"); + affiliation = xmpp_stanza_get_attribute(iq__x__item, "affiliation"); + jid = xmpp_stanza_get_attribute(iq__x__item, "jid"); + + user = user__search(account, from); + if (!user) + user = user__new(account, from, + channel && weechat_strcasecmp(from_bare, channel->id) == 0 + ? from_res : from); + user->profile.status_text = status ? strdup(status) : NULL; + user->profile.status = show ? strdup(show__text) : NULL; + user->profile.idle = idle ? strdup(idle__since) : NULL; + user->is_away = show ? weechat_strcasecmp(show__text, "away") == 0 : 0; + user->profile.role = role ? strdup(role) : NULL; + user->profile.affiliation = affiliation && strcmp(affiliation, "none") != 0 + ? strdup(affiliation) : NULL; + if (certificate && channel) + { + user->profile.pgp_id = pgp__verify(channel->buffer, account->pgp, certificate); + if (channel->type != CHANNEL_TYPE_MUC) + channel->pgp_id = user->profile.pgp_id; + } + + if (channel) + { + if (weechat_strcasecmp(role, "none") == 0) + channel__remove_member(account, channel, from, status); + else + channel__add_member(account, channel, from, jid ? jid : clientid); + } + } + else + { + user = user__search(account, from); + if (!user) + user = user__new(account, from, + channel && weechat_strcasecmp(from_bare, channel->id) == 0 + ? from_res : from); + user->profile.status_text = status ? strdup(status) : NULL; + user->profile.status = show ? strdup(show__text) : NULL; + user->profile.idle = idle ? strdup(idle__since) : NULL; + user->is_away = show ? weechat_strcasecmp(show__text, "away") == 0 : 0; + user->profile.role = role ? strdup(role) : NULL; + user->profile.affiliation = affiliation && strcmp(affiliation, "none") != 0 + ? strdup(affiliation) : NULL; + if (certificate && channel) + { + user->profile.pgp_id = pgp__verify(channel->buffer, account->pgp, certificate); + if (channel->type != CHANNEL_TYPE_MUC) + channel->pgp_id = user->profile.pgp_id; + } + + if (channel) + { + if (weechat_strcasecmp(type, "unavailable") == 0) + channel__remove_member(account, channel, from, status); + else + channel__add_member(account, channel, from, clientid); + } + } + + if (clientid) + free(clientid); + + return 1; +} + +int connection__message_handler(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata) +{ + (void) conn; + + struct t_account *account = (struct t_account *)userdata; + struct t_channel *channel; + xmpp_stanza_t *x, *body, *delay, *topic, *replace, *request, *markable, *composing, *sent, *received, *result, *forwarded; + const char *type, *from, *nick, *from_bare, *to, *to_bare, *id, *thread, *replace_id, *timestamp; + char *text, *intext, *difftext = NULL, *cleartext = NULL; + struct tm time = {0}; + time_t date = 0; + + body = xmpp_stanza_get_child_by_name(stanza, "body"); + if (body == NULL) + { + topic = xmpp_stanza_get_child_by_name(stanza, "subject"); + if (topic != NULL) + { + intext = xmpp_stanza_get_text(topic); + from = xmpp_stanza_get_from(stanza); + if (from == NULL) + return 1; + from_bare = xmpp_jid_bare(account->context, from); + from = xmpp_jid_resource(account->context, from); + channel = channel__search(account, from_bare); + if (!channel) + channel = channel__new(account, CHANNEL_TYPE_PM, from_bare, from_bare); + channel__update_topic(channel, intext ? intext : "", from, 0); + if (intext != NULL) + xmpp_free(account->context, intext); + } + + composing = xmpp_stanza_get_child_by_name_and_ns( + stanza, "composing", "http://jabber.org/protocol/chatstates"); + if (composing != NULL) + { + from = xmpp_stanza_get_from(stanza); + if (from == NULL) + return 1; + from_bare = xmpp_jid_bare(account->context, from); + nick = xmpp_jid_resource(account->context, from); + channel = channel__search(account, from_bare); + if (!channel) + return 1; + struct t_user *user = user__search(account, from); + if (!user) + user = user__new(account, from, + weechat_strcasecmp(from_bare, channel->id) == 0 + ? nick : from); + channel__add_typing(channel, user); + weechat_printf(channel->buffer, "...\t%s%s typing", + weechat_color("gray"), + channel->type == CHANNEL_TYPE_MUC ? nick : from); + } + + sent = xmpp_stanza_get_child_by_name_and_ns( + stanza, "sent", "urn:xmpp:carbons:2"); + if (sent) + forwarded = xmpp_stanza_get_child_by_name_and_ns( + sent, "forwarded", "urn:xmpp:forward:0"); + received = xmpp_stanza_get_child_by_name_and_ns( + stanza, "received", "urn:xmpp:carbons:2"); + if (received) + forwarded = xmpp_stanza_get_child_by_name_and_ns( + received, "forwarded", "urn:xmpp:forward:0"); + if ((sent || received) && forwarded != NULL) + { + xmpp_stanza_t *message = xmpp_stanza_get_children(forwarded); + return connection__message_handler(conn, message, userdata); + } + + result = xmpp_stanza_get_child_by_name_and_ns( + stanza, "result", "urn:xmpp:mam:2"); + if (result) + { + forwarded = xmpp_stanza_get_child_by_name_and_ns( + result, "forwarded", "urn:xmpp:forward:0"); + if (forwarded != NULL) + { + xmpp_stanza_t *message = xmpp_stanza_get_child_by_name(forwarded, "message"); + if (message) + { + message = xmpp_stanza_copy(message); + delay = xmpp_stanza_get_child_by_name_and_ns( + forwarded, "delay", "urn:xmpp:delay"); + if (delay != NULL) + xmpp_stanza_add_child_ex(message, xmpp_stanza_copy(delay), 0); + int ret = connection__message_handler(conn, message, userdata); + xmpp_stanza_release(message); + return ret; + } + } + } + + return 1; + } + type = xmpp_stanza_get_type(stanza); + if (type != NULL && strcmp(type, "error") == 0) + return 1; + from = xmpp_stanza_get_from(stanza); + if (from == NULL) + return 1; + from_bare = xmpp_jid_bare(account->context, from); + to = xmpp_stanza_get_to(stanza); + if (to == NULL) + to = account_jid(account); + to_bare = to ? xmpp_jid_bare(account->context, to) : NULL; + id = xmpp_stanza_get_id(stanza); + thread = xmpp_stanza_get_attribute(stanza, "thread"); + replace = xmpp_stanza_get_child_by_name_and_ns(stanza, "replace", + "urn:xmpp:message-correct:0"); + replace_id = replace ? xmpp_stanza_get_id(replace) : NULL; + request = xmpp_stanza_get_child_by_name_and_ns(stanza, "request", + "urn:xmpp:receipts"); + markable = xmpp_stanza_get_child_by_name_and_ns(stanza, "markable", + "urn:xmpp:chat-markers:0"); + + const char *channel_id = weechat_strcasecmp(account_jid(account), from_bare) + == 0 ? to_bare : from_bare; + channel = channel__search(account, channel_id); + if (!channel) + channel = channel__new(account, + weechat_strcasecmp(type, "groupchat") == 0 + ? CHANNEL_TYPE_MUC : CHANNEL_TYPE_PM, + channel_id, channel_id); + + if (id && (markable || request)) + { + struct t_channel_unread *unread = malloc(sizeof(struct t_channel_unread)); + unread->id = strdup(id); + unread->thread = thread ? strdup(thread) : NULL; + + xmpp_stanza_t *message = xmpp_message_new(account->context, NULL, + channel->id, NULL); + + if (request) + { + xmpp_stanza_t *message__received = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__received, "received"); + xmpp_stanza_set_ns(message__received, "urn:xmpp:receipts"); + xmpp_stanza_set_id(message__received, unread->id); + + xmpp_stanza_add_child(message, message__received); + xmpp_stanza_release(message__received); + } + + if (markable) + { + xmpp_stanza_t *message__received = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__received, "received"); + xmpp_stanza_set_ns(message__received, "urn:xmpp:chat-markers:0"); + xmpp_stanza_set_id(message__received, unread->id); + + xmpp_stanza_add_child(message, message__received); + xmpp_stanza_release(message__received); + } + + if (unread->thread) + { + xmpp_stanza_t *message__thread = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(message__thread, "thread"); + + xmpp_stanza_t *message__thread__text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(message__thread__text, unread->thread); + xmpp_stanza_add_child(message__thread, message__thread__text); + xmpp_stanza_release(message__thread__text); + + xmpp_stanza_add_child(message, message__thread); + xmpp_stanza_release(message__thread); + } + + xmpp_send(account->connection, message); + xmpp_stanza_release(message); + + if (!channel->unreads) + channel->unreads = weechat_list_new(); + weechat_list_add(channel->unreads, unread->id, WEECHAT_LIST_POS_END, unread); + } + + x = xmpp_stanza_get_child_by_name_and_ns(stanza, "x", "jabber:x:encrypted"); + intext = xmpp_stanza_get_text(body); + if (x) + { + char *ciphertext = xmpp_stanza_get_text(x); + cleartext = pgp__decrypt(channel->buffer, account->pgp, ciphertext); + xmpp_free(account->context, ciphertext); + } + text = cleartext ? cleartext : intext; + + if (replace) + { + const char *orig = NULL; + void *lines = weechat_hdata_pointer(weechat_hdata_get("buffer"), + channel->buffer, "lines"); + if (lines) + { + void *last_line = weechat_hdata_pointer(weechat_hdata_get("lines"), + lines, "last_line"); + while (last_line && !orig) + { + void *line_data = weechat_hdata_pointer(weechat_hdata_get("line"), + last_line, "data"); + if (line_data) + { + int tags_count = weechat_hdata_integer(weechat_hdata_get("line_data"), + line_data, "tags_count"); + char str_tag[20] = {0}; + for (int n_tag = 0; n_tag < tags_count; n_tag++) + { + snprintf(str_tag, sizeof(str_tag), "%d|tags_array", n_tag); + const char *tag = weechat_hdata_string(weechat_hdata_get("line_data"), + line_data, str_tag); + if (strlen(tag) > strlen("id_") && + weechat_strcasecmp(tag+strlen("id_"), replace_id) == 0) + { + struct t_arraylist *orig_lines = weechat_arraylist_new( + 0, 0, 0, NULL, NULL, NULL, NULL); + char *msg = (char*)weechat_hdata_string(weechat_hdata_get("line_data"), + line_data, "message"); + weechat_arraylist_insert(orig_lines, 0, msg); + + while (msg) + { + last_line = weechat_hdata_pointer(weechat_hdata_get("line"), + last_line, "prev_line"); + if (last_line) + line_data = weechat_hdata_pointer(weechat_hdata_get("line"), + last_line, "data"); + else + line_data = NULL; + + msg = NULL; + if (line_data) + { + tags_count = weechat_hdata_integer(weechat_hdata_get("line_data"), + line_data, "tags_count"); + for (n_tag = 0; n_tag < tags_count; n_tag++) + { + snprintf(str_tag, sizeof(str_tag), "%d|tags_array", n_tag); + tag = weechat_hdata_string(weechat_hdata_get("line_data"), + line_data, str_tag); + if (strlen(tag) > strlen("id_") && + weechat_strcasecmp(tag+strlen("id_"), replace_id) == 0) + { + msg = (char*)weechat_hdata_string(weechat_hdata_get("line_data"), + line_data, "message"); + break; + } + } + } + + if (msg) + weechat_arraylist_insert(orig_lines, 0, msg); + } + + char **orig_message = weechat_string_dyn_alloc(256); + for (int i = 0; i < weechat_arraylist_size(orig_lines); i++) + weechat_string_dyn_concat(orig_message, + weechat_arraylist_get(orig_lines, i), + -1); + orig = *orig_message; + weechat_string_dyn_free(orig_message, 0); + break; + } + } + } + + last_line = weechat_hdata_pointer(weechat_hdata_get("line"), + last_line, "prev_line"); + } + } + + if (orig) + { + struct diff result; + if (diff(&result, char_cmp, 1, orig, strlen(orig), text, strlen(text)) > 0) + { + char **visual = weechat_string_dyn_alloc(256); + char ch[2] = {0}; + + for (size_t i = 0; i < result.sessz; i++) + switch (result.ses[i].type) + { + case DIFF_ADD: + weechat_string_dyn_concat(visual, weechat_color("green"), -1); + *ch = *(const char *)result.ses[i].e; + weechat_string_dyn_concat(visual, ch, -1); + break; + case DIFF_DELETE: + weechat_string_dyn_concat(visual, weechat_color("red"), -1); + *ch = *(const char *)result.ses[i].e; + weechat_string_dyn_concat(visual, ch, -1); + break; + case DIFF_COMMON: + default: + weechat_string_dyn_concat(visual, weechat_color("resetcolor"), -1); + *ch = *(const char *)result.ses[i].e; + + weechat_string_dyn_concat(visual, ch, -1); + break; + } + free(result.ses); + free(result.lcs); + + difftext = strdup(*visual); + weechat_string_dyn_free(visual, 1); + } + } + } + + nick = from; + if (weechat_strcasecmp(type, "groupchat") == 0) + { + nick = weechat_strcasecmp(channel->name, + xmpp_jid_bare(account->context, + from)) == 0 + ? xmpp_jid_resource(account->context, from) + : from; + } + delay = xmpp_stanza_get_child_by_name_and_ns(stanza, "delay", "urn:xmpp:delay"); + timestamp = delay ? xmpp_stanza_get_attribute(delay, "stamp") : NULL; + if (timestamp) + { + strptime(timestamp, "%FT%T", &time); + date = mktime(&time); + } + + char **dyn_tags = weechat_string_dyn_alloc(1); + weechat_string_dyn_concat(dyn_tags, "xmpp_message,message", -1); + { + weechat_string_dyn_concat(dyn_tags, ",nick_", -1); + weechat_string_dyn_concat(dyn_tags, nick, -1); + } + { + weechat_string_dyn_concat(dyn_tags, ",host_", -1); + weechat_string_dyn_concat(dyn_tags, from, -1); + } + if (id) + { + weechat_string_dyn_concat(dyn_tags, ",id_", -1); + weechat_string_dyn_concat(dyn_tags, id, -1); + } + + if (channel->type == CHANNEL_TYPE_PM) + weechat_string_dyn_concat(dyn_tags, ",private", -1); + if (weechat_string_match(text, "/me *", 0)) + weechat_string_dyn_concat(dyn_tags, ",xmpp_action", -1); + if (replace) + { + weechat_string_dyn_concat(dyn_tags, ",edit", -1); + weechat_string_dyn_concat(dyn_tags, ",replace_", -1); + weechat_string_dyn_concat(dyn_tags, replace_id, -1); + } + + if (date != 0) + weechat_string_dyn_concat(dyn_tags, ",notify_none", -1); + else if (channel->type == CHANNEL_TYPE_PM + && weechat_strcasecmp(from_bare, account_jid(account)) != 0) + weechat_string_dyn_concat(dyn_tags, ",notify_private", -1); + else + weechat_string_dyn_concat(dyn_tags, ",log1", -1); + + const char *edit = replace ? "* " : ""; // Losing which message was edited, sadly + if (x && text == cleartext && channel->transport != CHANNEL_TRANSPORT_PGP) + { + channel->transport = CHANNEL_TRANSPORT_PGP; + weechat_printf_date_tags(channel->buffer, date, NULL, "%s%sTransport: %s", + weechat_prefix("network"), weechat_color("gray"), + channel__transport_name(channel->transport)); + } + else if (!x && text == intext && channel->transport != CHANNEL_TRANSPORT_PLAINTEXT) + { + channel->transport = CHANNEL_TRANSPORT_PLAINTEXT; + weechat_printf_date_tags(channel->buffer, date, NULL, "%s%sTransport: %s", + weechat_prefix("network"), weechat_color("gray"), + channel__transport_name(channel->transport)); + } + if (channel_id == from_bare && strcmp(to, channel->id) == 0) + weechat_printf_date_tags(channel->buffer, date, *dyn_tags, "%s%s\t[to %s]: %s", + edit, user__as_prefix_raw(account, nick), + to, difftext ? difftext : text ? text : ""); + else if (weechat_string_match(text, "/me *", 0)) + weechat_printf_date_tags(channel->buffer, date, *dyn_tags, "%s%s\t%s %s", + edit, weechat_prefix("action"), user__as_prefix_raw(account, nick), + difftext ? difftext+4 : text ? text+4 : ""); + else + weechat_printf_date_tags(channel->buffer, date, *dyn_tags, "%s%s\t%s", + edit, user__as_prefix_raw(account, nick), + difftext ? difftext : text ? text : ""); + + weechat_string_dyn_free(dyn_tags, 1); + + if (intext) + xmpp_free(account->context, intext); + if (difftext) + free(difftext); + if (cleartext) + free(cleartext); + + return 1; +} + +int connection__iq_handler(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata) +{ + struct t_account *account = (struct t_account *)userdata; + xmpp_stanza_t *reply, *query, *identity, *feature, *x, *field, *value, *text, *fin; + xmpp_stanza_t *pubsub, *items, *item, *list, *device, **children; + xmpp_stanza_t *storage, *conference, *nick; + static struct utsname osinfo; + + const char *id = xmpp_stanza_get_id(stanza); + query = xmpp_stanza_get_child_by_name_and_ns( + stanza, "query", "http://jabber.org/protocol/disco#info"); + const char *type = xmpp_stanza_get_attribute(stanza, "type"); + if (query && type && weechat_strcasecmp(type, "get") == 0) + { + char *client_name; + + reply = xmpp_stanza_reply(stanza); + xmpp_stanza_set_type(reply, "result"); + + client_name = weechat_string_eval_expression("weechat ${info:version}", + NULL, NULL, NULL); + + identity = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(identity, "identity"); + xmpp_stanza_set_attribute(identity, "category", "client"); + xmpp_stanza_set_attribute(identity, "name", client_name); + xmpp_stanza_set_attribute(identity, "type", "pc"); + xmpp_stanza_add_child(query, identity); + xmpp_stanza_release(identity); + +#define FEATURE(ns) \ + feature = xmpp_stanza_new(account->context); \ + xmpp_stanza_set_name(feature, "feature"); \ + xmpp_stanza_set_attribute(feature, "var", ns); \ + xmpp_stanza_add_child(query, feature); \ + xmpp_stanza_release(feature); + + FEATURE("eu.siacs.conversations.axolotl.devicelist+notify"); + FEATURE("http://jabber.org/protocol/caps"); + FEATURE("http://jabber.org/protocol/chatstates"); + FEATURE("http://jabber.org/protocol/disco#info"); + FEATURE("http://jabber.org/protocol/disco#items"); + FEATURE("http://jabber.org/protocol/muc"); + FEATURE("http://jabber.org/protocol/nick+notify"); + FEATURE("jabber:iq:version"); + FEATURE("jabber:x:conference"); + FEATURE("jabber:x:oob"); + FEATURE("storage:bookmarks+notify"); + FEATURE("urn:xmpp:avatar:metadata+notify"); + FEATURE("urn:xmpp:chat-markers:0"); + FEATURE("urn:xmpp:idle:1"); + //FEATURE("urn:xmpp:jingle-message:0"); + //FEATURE("urn:xmpp:jingle:1"); + //FEATURE("urn:xmpp:jingle:apps:dtls:0"); + //FEATURE("urn:xmpp:jingle:apps:file-transfer:3"); + //FEATURE("urn:xmpp:jingle:apps:file-transfer:4"); + //FEATURE("urn:xmpp:jingle:apps:file-transfer:5"); + //FEATURE("urn:xmpp:jingle:apps:rtp:1"); + //FEATURE("urn:xmpp:jingle:apps:rtp:audio"); + //FEATURE("urn:xmpp:jingle:apps:rtp:video"); + //FEATURE("urn:xmpp:jingle:jet-omemo:0"); + //FEATURE("urn:xmpp:jingle:jet:0"); + //FEATURE("urn:xmpp:jingle:transports:ibb:1"); + //FEATURE("urn:xmpp:jingle:transports:ice-udp:1"); + //FEATURE("urn:xmpp:jingle:transports:s5b:1"); + FEATURE("urn:xmpp:message-correct:0"); + FEATURE("urn:xmpp:ping"); + FEATURE("urn:xmpp:receipts"); + FEATURE("urn:xmpp:time"); +#undef FEATURE + + x = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(x, "x"); + xmpp_stanza_set_ns(x, "jabber:x:data"); + xmpp_stanza_set_attribute(x, "type", "result"); + + if (uname(&osinfo) < 0) + { + *osinfo.sysname = 0; + *osinfo.release = 0; + } + + // This is utter bullshit, TODO: anything but this. + { + field = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(field, "field"); + xmpp_stanza_set_attribute(field, "var", "FORM_TYPE"); + xmpp_stanza_set_attribute(field, "type", "hidden"); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, "urn:xmpp:dataforms:softwareinfo"); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + xmpp_stanza_add_child(x, field); + xmpp_stanza_release(field); + } + + { + field = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(field, "field"); + xmpp_stanza_set_attribute(field, "var", "ip_version"); + xmpp_stanza_set_attribute(field, "type", "text-multi"); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, "ipv4"); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, "ipv6"); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + xmpp_stanza_add_child(x, field); + xmpp_stanza_release(field); + } + + { + field = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(field, "field"); + xmpp_stanza_set_attribute(field, "var", "os"); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, osinfo.sysname); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + xmpp_stanza_add_child(x, field); + xmpp_stanza_release(field); + } + + { + field = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(field, "field"); + xmpp_stanza_set_attribute(field, "var", "os_version"); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, osinfo.release); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + xmpp_stanza_add_child(x, field); + xmpp_stanza_release(field); + } + + { + field = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(field, "field"); + xmpp_stanza_set_attribute(field, "var", "software"); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, "weechat"); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + xmpp_stanza_add_child(x, field); + xmpp_stanza_release(field); + } + + { + field = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(field, "field"); + xmpp_stanza_set_attribute(field, "var", "software_version"); + + value = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(value, "value"); + + text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(text, weechat_info_get("version", NULL)); + xmpp_stanza_add_child(value, text); + xmpp_stanza_release(text); + + xmpp_stanza_add_child(field, value); + xmpp_stanza_release(value); + + xmpp_stanza_add_child(x, field); + xmpp_stanza_release(field); + } + + xmpp_stanza_add_child(query, x); + xmpp_stanza_release(x); + + xmpp_stanza_add_child(reply, query); + + xmpp_send(conn, reply); + xmpp_stanza_release(reply); + + free(client_name); + } + + pubsub = xmpp_stanza_get_child_by_name_and_ns( + stanza, "pubsub", "http://jabber.org/protocol/pubsub"); + if (pubsub) + { + const char *items_node, *item_id, *device_id, *ns, *node; + + items = xmpp_stanza_get_child_by_name(pubsub, "items"); + if (items) + { + items_node = xmpp_stanza_get_attribute(items, "node"); + if (items_node + && weechat_strcasecmp(items_node, + "eu.siacs.conversations.axolotl.devicelist") == 0) + { + item = xmpp_stanza_get_child_by_name(items, "item"); + if (item) + item_id = xmpp_stanza_get_id(item); + if (item && item_id && weechat_strcasecmp(item_id, "current") == 0) + { + list = xmpp_stanza_get_child_by_name_and_ns( + item, "list", "eu.siacs.conversations.axolotl"); + if (list && account->omemo) + { + account__free_device_all(account); + + struct t_account_device *dev; + char id[64] = {0}; + int i = 0; + + dev = malloc(sizeof(struct t_account_device)); + + dev->id = account->omemo->device_id; + snprintf(id, sizeof(id), "%d", dev->id); + dev->name = strdup(id); + account__add_device(account, dev); + + children = malloc(sizeof(xmpp_stanza_t *) * 128); + children[i++] = stanza__iq_pubsub_publish_item_list_device( + account->context, NULL, with_noop(dev->name)); + + free(dev->name); + free(dev); + + for (device = xmpp_stanza_get_children(list); + device; device = xmpp_stanza_get_next(device)) + { + const char *name = xmpp_stanza_get_name(device); + if (weechat_strcasecmp(name, "device") != 0) + continue; + + device_id = xmpp_stanza_get_id(device); + + dev = malloc(sizeof(struct t_account_device)); + dev->id = atoi(device_id); + dev->name = strdup(device_id); + account__add_device(account, dev); + + children[i++] = stanza__iq_pubsub_publish_item_list_device( + account->context, NULL, with_noop(dev->name)); + + free(dev->name); + free(dev); + } + + children[i] = NULL; + node = "eu.siacs.conversations.axolotl"; + children[0] = stanza__iq_pubsub_publish_item_list( + account->context, NULL, children, with_noop(node)); + children[1] = NULL; + children[0] = stanza__iq_pubsub_publish_item( + account->context, NULL, children, with_noop("current")); + ns = "http://jabber.org/protocol/pubsub"; + children[0] = stanza__iq_pubsub_publish(account->context, + NULL, children, + with_noop(ns)); + children[0] = stanza__iq_pubsub(account->context, NULL, + children, with_noop("")); + reply = stanza__iq(account->context, xmpp_stanza_reply(stanza), + children, NULL, strdup("announce1"), + NULL, NULL, strdup("set")); + + xmpp_send(conn, reply); + xmpp_stanza_release(reply); + + char bundle_node[128] = {0}; + snprintf(bundle_node, sizeof(bundle_node), + "eu.siacs.conversations.axolotl.bundles:%d", + account->omemo->device_id); + + xmpp_stanza_t *textchild[2] = {NULL}; + textchild[0] = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(textchild[0], "b64enc1"); + children[0] = stanza__iq_pubsub_publish_item_bundle_signedPreKeyPublic( + account->context, NULL, textchild, with_noop("1")); + textchild[0] = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(textchild[0], "b64enc2"); + children[1] = stanza__iq_pubsub_publish_item_bundle_signedPreKeySignature( + account->context, NULL, textchild); + textchild[0] = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(textchild[0], "b64enc3"); + children[2] = stanza__iq_pubsub_publish_item_bundle_identityKey( + account->context, NULL, textchild); + textchild[0] = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(textchild[0], "b64enc4"); + children[3] = stanza__iq_pubsub_publish_item_bundle_prekeys_preKeyPublic( + account->context, NULL, textchild, with_noop("1")); + textchild[0] = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(textchild[0], "b64enc5"); + children[4] = stanza__iq_pubsub_publish_item_bundle_prekeys_preKeyPublic( + account->context, NULL, textchild, with_noop("2")); + textchild[0] = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(textchild[0], "b64enc6"); + children[5] = stanza__iq_pubsub_publish_item_bundle_prekeys_preKeyPublic( + account->context, NULL, textchild, with_noop("3")); + children[6] = NULL; + children[3] = stanza__iq_pubsub_publish_item_bundle_prekeys( + account->context, NULL, &children[3]); + children[4] = NULL; + ns = "eu.siacs.conversations.axolotl"; + children[0] = stanza__iq_pubsub_publish_item_bundle( + account->context, NULL, children, with_noop(ns)); + children[1] = NULL; + children[0] = stanza__iq_pubsub_publish_item( + account->context, NULL, children, with_noop("current")); + children[0] = stanza__iq_pubsub_publish(account->context, + NULL, children, + with_noop(bundle_node)); + children[0] = + stanza__iq_pubsub(account->context, NULL, children, + with_noop("http://jabber.org/protocol/pubsub")); + children[0] = + stanza__iq(account->context, NULL, children, NULL, strdup("announce2"), + strdup(account_jid(account)), strdup(account_jid(account)), + strdup("set")); + + xmpp_send(conn, children[0]); + xmpp_stanza_release(children[0]); + + free(children); + } + } + } + if (items_node + && weechat_strcasecmp(items_node, "storage:bookmarks") == 0) + { + item = xmpp_stanza_get_child_by_name(items, "item"); + if (item) + item_id = xmpp_stanza_get_id(item); + if (item && item_id && weechat_strcasecmp(item_id, "current") == 0) + { + storage = xmpp_stanza_get_child_by_name_and_ns( + item, "storage", "storage:bookmarks"); + if (storage) + { + for (conference = xmpp_stanza_get_children(storage); + conference; conference = xmpp_stanza_get_next(conference)) + { + const char *name = xmpp_stanza_get_name(conference); + if (weechat_strcasecmp(name, "conference") != 0) + continue; + + const char *jid = xmpp_stanza_get_attribute(conference, "jid"); + const char *autojoin = xmpp_stanza_get_attribute(conference, "autojoin"); + name = xmpp_stanza_get_attribute(conference, "name"); + nick = xmpp_stanza_get_child_by_name(conference, "nick"); + char *intext; + if (nick) + { + text = xmpp_stanza_get_children(nick); + intext = xmpp_stanza_get_text(text); + } + + if (weechat_strcasecmp(autojoin, "true") == 0) + { + char **command = weechat_string_dyn_alloc(256); + weechat_string_dyn_concat(command, "/enter ", -1); + weechat_string_dyn_concat(command, jid, -1); + if (nick) + { + weechat_string_dyn_concat(command, "/", -1); + weechat_string_dyn_concat(command, intext, -1); + } + weechat_command(account->buffer, *command); + weechat_string_dyn_free(command, 1); + } + + if (nick) + free(intext); + } + } + } + } + } + } + + fin = xmpp_stanza_get_child_by_name_and_ns( + stanza, "fin", "urn:xmpp:mam:2"); + if (fin) + { + xmpp_stanza_t *set, *set__last; + char *set__last__text; + struct t_account_mam_query *mam_query; + + set = xmpp_stanza_get_child_by_name_and_ns( + fin, "set", "http://jabber.org/protocol/rsm"); + mam_query = account__mam_query_search(account, id); + if (set && mam_query) + { + struct t_channel *channel = channel__search(account, + mam_query->with); + + set__last = xmpp_stanza_get_child_by_name(set, "last"); + set__last__text = set__last + ? xmpp_stanza_get_text(set__last) : NULL; + + if (channel && set__last__text) + { + channel__fetch_mam(account, channel, id, + mam_query->has_start ? &mam_query->start : NULL, + mam_query->has_end ? &mam_query->end : NULL, + set__last__text); + } + else if (!set__last) + account__mam_query_free(account, mam_query); + } + } + + return 1; +} + +void connection__handler(xmpp_conn_t *conn, xmpp_conn_event_t status, + int error, xmpp_stream_error_t *stream_error, + void *userdata) +{ + struct t_account *account = (struct t_account *)userdata; + + (void)error; + (void)stream_error; + + if (status == XMPP_CONN_CONNECT) + { + account->disconnected = 0; + + xmpp_stanza_t *pres, *pres__c, *pres__status, *pres__status__text, + *pres__x, *pres__x__text, **children; + char cap_hash[28+1] = {0}; + + xmpp_handler_add(conn, &connection__version_handler, + "jabber:iq:version", "iq", NULL, account); + xmpp_handler_add(conn, &connection__presence_handler, + NULL, "presence", NULL, account); + xmpp_handler_add(conn, &connection__message_handler, + NULL, "message", /*type*/ NULL, account); + //xmpp_handler_add(conn, &connection__iq_handler, + // NULL, "iq", "get", account); + xmpp_handler_add(conn, &connection__iq_handler, + NULL, "iq", NULL, account); + + pgp__init(&account->pgp, + weechat_string_eval_expression(account_pgp_pubring_path(account), + NULL, NULL, NULL), + weechat_string_eval_expression(account_pgp_secring_path(account), + NULL, NULL, NULL)); + + /* Send initial so that we appear online to contacts */ + children = malloc(sizeof(*children) * (3 + 1)); + + pres__c = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(pres__c, "c"); + xmpp_stanza_set_ns(pres__c, "http://jabber.org/protocol/caps"); + xmpp_stanza_set_attribute(pres__c, "hash", "sha-1"); + xmpp_stanza_set_attribute(pres__c, "node", "http://weechat.org"); + snprintf(cap_hash, sizeof(cap_hash), "%027ld=", time(NULL)); + xmpp_stanza_set_attribute(pres__c, "ver", cap_hash); + children[0] = pres__c; + + pres__status = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(pres__status, "status"); + + pres__status__text = xmpp_stanza_new(account->context); + xmpp_stanza_set_text(pres__status__text, account_status(account)); + xmpp_stanza_add_child(pres__status, pres__status__text); + xmpp_stanza_release(pres__status__text); + + children[1] = pres__status; + children[2] = NULL; + + if (account->pgp) + { + pres__x = xmpp_stanza_new(account->context); + xmpp_stanza_set_name(pres__x, "x"); + xmpp_stanza_set_ns(pres__x, "jabber:x:signed"); + + pres__x__text = xmpp_stanza_new(account->context); + char *signature = pgp__sign(account->buffer, account->pgp, account_pgp_keyid(account), account_status(account)); + xmpp_stanza_set_text(pres__x__text, signature ? signature : ""); + free(signature); + xmpp_stanza_add_child(pres__x, pres__x__text); + xmpp_stanza_release(pres__x__text); + + children[2] = pres__x; + children[3] = NULL; + } + + pres = stanza__presence(account->context, NULL, + children, NULL, strdup(account_jid(account)), + NULL, NULL); + xmpp_send(conn, pres); + xmpp_stanza_release(pres); + + children[1] = NULL; + children[0] = + stanza__iq_enable(account->context, NULL, with_noop("urn:xmpp:carbons:2")); + children[0] = + stanza__iq(account->context, NULL, children, + strdup("jabber:client"), strdup("enable1"), + strdup(account_jid(account)), NULL, strdup("set")); + + xmpp_send(conn, children[0]); + xmpp_stanza_release(children[0]); + + children[1] = NULL; + children[0] = + stanza__iq_pubsub_items(account->context, NULL, + strdup("storage:bookmarks")); + children[0] = + stanza__iq_pubsub(account->context, NULL, children, + with_noop("http://jabber.org/protocol/pubsub")); + children[0] = + stanza__iq(account->context, NULL, children, NULL, strdup("retrieve1"), + strdup(account_jid(account)), strdup(account_jid(account)), + strdup("get")); + + xmpp_send(conn, children[0]); + xmpp_stanza_release(children[0]); + + children[1] = NULL; + children[0] = + stanza__iq_pubsub_items(account->context, NULL, + strdup("eu.siacs.conversations.axolotl.devicelist")); + children[0] = + stanza__iq_pubsub(account->context, NULL, children, + with_noop("http://jabber.org/protocol/pubsub")); + children[0] = + stanza__iq(account->context, NULL, children, NULL, strdup("fetch1"), + strdup(account_jid(account)), strdup(account_jid(account)), + strdup("get")); + + xmpp_send(conn, children[0]); + xmpp_stanza_release(children[0]); + + omemo__init(account->buffer, &account->omemo, account->name); + + (void) weechat_hook_signal_send("xmpp_account_connected", + WEECHAT_HOOK_SIGNAL_STRING, account->name); + } + else + { + account__disconnect(account, 1); + //xmpp_stop(account->context); //keep context? + } +} + +char* connection__rand_string(int length) +{ + char *string = malloc(length); + srand(time(NULL)); + for(int i = 0; i < length; ++i){ + string[i] = '0' + rand()%72; // starting on '0', ending on '}' + if (!((string[i] >= '0' && string[i] <= '9') || + (string[i] >= 'A' && string[i] <= 'Z') || + (string[i] >= 'a' && string[i] <= 'z'))) + i--; // reroll + } + string[length] = 0; + return string; +} + +int connection__connect(struct t_account *account, xmpp_conn_t **connection, + const char* jid, const char* password, int tls) +{ + static const unsigned ka_timeout_sec = 60; + static const unsigned ka_timeout_ivl = 1; + + *connection = xmpp_conn_new(account->context); + + xmpp_conn_set_keepalive(*connection, ka_timeout_sec, ka_timeout_ivl); + + const char *resource = account_resource(account); + if (!(resource && strlen(resource))) + { + char *const rand = connection__rand_string(8); + char ident[64] = {0}; + snprintf(ident, sizeof(ident), "weechat.%s", rand); + free(rand); + + account_option_set(account, ACCOUNT_OPTION_RESOURCE, ident); + resource = account_resource(account); + } + xmpp_conn_set_jid(*connection, + xmpp_jid_new(account->context, + xmpp_jid_node(account->context, jid), + xmpp_jid_domain(account->context, jid), + resource)); + xmpp_conn_set_pass(*connection, password); + + int flags = xmpp_conn_get_flags(*connection); + switch (tls) + { + case 0: + flags |= XMPP_CONN_FLAG_DISABLE_TLS; + break; + case 1: + flags &= ~XMPP_CONN_FLAG_DISABLE_TLS; + flags &= ~XMPP_CONN_FLAG_TRUST_TLS; + break; + case 2: + flags |= XMPP_CONN_FLAG_TRUST_TLS; + break; + default: + break; + } + xmpp_conn_set_flags(*connection, flags); + + if (xmpp_connect_client(*connection, NULL, 0, &connection__handler, account) + != XMPP_EOK) + { + weechat_printf( + NULL, + _("%s%s: error connecting to %s"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, + jid); + return 0; + } + + return 1; +} + +void connection__process(xmpp_ctx_t *context, xmpp_conn_t *connection, + const unsigned long timeout) +{ + if (connection) + { + xmpp_run_once(context ? context : xmpp_conn_get_context(connection), + timeout); + } +} diff --git a/connection.h b/connection.h new file mode 100644 index 0000000..54eb1dc --- /dev/null +++ b/connection.h @@ -0,0 +1,16 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_CONNECTION_H_ +#define _WEECHAT_XMPP_CONNECTION_H_ + +void connection__init(); + +int connection__connect(struct t_account *account, xmpp_conn_t **connection, + const char* jid, const char* password, int tls); + +void connection__process(xmpp_ctx_t *context, xmpp_conn_t *connection, + const unsigned long timeout); + +#endif /*WEECHAT_XMPP_CONNECTION_H*/ diff --git a/input.c b/input.c new file mode 100644 index 0000000..dc9fd6f --- /dev/null +++ b/input.c @@ -0,0 +1,84 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include + +#include "plugin.h" +#include "account.h" +#include "channel.h" +#include "buffer.h" +#include "message.h" +#include "input.h" + +int input__data(struct t_gui_buffer *buffer, const char *text) +{ + struct t_account *account = NULL; + struct t_channel *channel = NULL; + + buffer__get_account_and_channel(buffer, &account, &channel); + + if (!account) + return WEECHAT_RC_ERROR; + + if (channel) + { + if (!account->is_connected) + { + weechat_printf(buffer, + _("%s%s: you are not connected to server"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return WEECHAT_RC_OK; + } + + channel__send_message(account, channel, channel->id, text); + } + else + { + weechat_printf(buffer, + _("%s%s: this buffer is not a channel!"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + } + + return WEECHAT_RC_OK; +} + +int input__data_cb(const void *pointer, void *data, + struct t_gui_buffer *buffer, + const char *text) +{ + (void) pointer; + (void) data; + + return input__data(buffer, text); +} + +int input__typing(struct t_gui_buffer *buffer) +{ + struct t_account *account = NULL; + struct t_channel *channel = NULL; + + buffer__get_account_and_channel(buffer, &account, &channel); + + if (account && account->is_connected && channel) + { + channel__send_reads(account, channel); + channel__send_typing(account, channel, NULL); + } + + return WEECHAT_RC_OK; +} + +int input__text_changed_cb(const void *pointer, void *data, + const char *signal, const char *type_data, + void *signal_data) +{ + (void) pointer; + (void) data; + (void) signal; + (void) type_data; + + return input__typing(signal_data); +} diff --git a/input.h b/input.h new file mode 100644 index 0000000..50bdd01 --- /dev/null +++ b/input.h @@ -0,0 +1,16 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_INPUT_H_ +#define _WEECHAT_XMPP_INPUT_H_ + +int input__data_cb(const void *pointer, void *data, + struct t_gui_buffer *buffer, + const char *input_data); + +int input__text_changed_cb(const void *pointer, void *data, + const char *signal, const char *type_data, + void *signal_data); + +#endif /*WEECHAT_XMPP_INPUT_H*/ diff --git a/message.c b/message.c new file mode 100644 index 0000000..0314d44 --- /dev/null +++ b/message.c @@ -0,0 +1,249 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include + +#include "plugin.h" +#include "account.h" +#include "channel.h" +#include "user.h" +#include "message.h" + +static const char format_regex[] = "<([^>]*?)>"; +static const size_t max_groups = 2; + +char *message__translate_code(struct t_account *account, + const char *code) +{ + struct t_channel *channel; + struct t_user *user; + size_t resultlen; + char *identifier, *alttext, *result, *symbol, *prefix; + + identifier = strdup(code); + alttext = strchr(identifier, '|'); + if (alttext) + *alttext++ = '\0'; + + switch (identifier[0]) + { + case '#': /* channel */ + if (alttext) + { + prefix = "#"; + symbol = strdup(alttext); + } + else + { + channel = channel__search(account, identifier+1); + if (channel) + { + prefix = "#"; + symbol = strdup(channel->name); + } + else + { + prefix = "Channel:"; + symbol = strdup(identifier+1); + } + } + break; + case '@': /* user */ + if (alttext) + { + prefix = "@"; + symbol = strdup(alttext); + } + else + { + user = user__search(account, identifier+1); + if (user) + { + prefix = "@"; + symbol = strdup(user->profile.display_name); + } + else + { + prefix = "User:"; + symbol = strdup(identifier+1); + } + } + break; + case '!': /* special */ + if (alttext) + { + prefix = "@"; + symbol = strdup(alttext); + } + else + { + prefix = "@"; + symbol = strdup(identifier+1); + } + break; + default: /* url */ + prefix = ""; + symbol = strdup(code); + break; + } + + free(identifier); + resultlen = snprintf(NULL, 0, "%s%s%s%s", weechat_color("chat_nick"), prefix, symbol, weechat_color("reset")) + 1; + result = malloc(resultlen); + snprintf(result, resultlen, "%s%s%s%s", weechat_color("chat_nick"), prefix, symbol, weechat_color("reset")); + free(symbol); + + return result; +} + +void message__htmldecode(char *dest, const char *src, size_t n) +{ + size_t i, j; + + for (i = 0, j = 0; i < n; i++, j++) + switch (src[i]) + { + case '\0': + dest[j] = '\0'; + return; + case '&': + if (src[i+1] == 'g' && + src[i+2] == 't' && + src[i+3] == ';') + { + dest[j] = '>'; + i += 3; + break; + } + else if (src[i+1] == 'l' && + src[i+2] == 't' && + src[i+3] == ';') + { + dest[j] = '<'; + i += 3; + break; + } + else if (src[i+1] == 'a' && + src[i+2] == 'm' && + src[i+3] == 'p' && + src[i+4] == ';') + { + dest[j] = '&'; + i += 4; + break; + } + /* fallthrough */ + default: + dest[j] = src[i]; + break; + } + dest[j-1] = '\0'; + return; +} + +char *message__decode(struct t_account *account, + const char *text) +{ + int rc; + regex_t reg; + regmatch_t groups[max_groups]; + char msgbuf[100]; + char *decoded_text; + const char *cursor; + size_t offset; + + if ((rc = regcomp(®, format_regex, REG_EXTENDED))) + { + regerror(rc, ®, msgbuf, sizeof(msgbuf)); + weechat_printf( + account->buffer, + _("%s%s: error compiling message formatting regex: %s"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME, + msgbuf); + return strdup(text); + } + + decoded_text = malloc(MESSAGE_MAX_LENGTH); + if (!decoded_text) + { + regfree(®); + weechat_printf( + account->buffer, + _("%s%s: error allocating space for message"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return strdup(text); + } + decoded_text[0] = '\0'; + + for (cursor = text; regexec(®, cursor, max_groups, groups, 0) == 0; cursor += offset) + { + offset = groups[0].rm_eo; + + char *copy = strdup(cursor); + if (!copy) + { + regfree(®); + free(decoded_text); + weechat_printf( + account->buffer, + _("%s%s: error allocating space for message"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return strdup(text); + } + copy[groups[1].rm_eo] = '\0'; + + char *match = strdup(copy + groups[1].rm_so); + if (!match) + { + free(copy); + regfree(®); + free(decoded_text); + weechat_printf( + account->buffer, + _("%s%s: error allocating space for message"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return strdup(text); + } + copy[groups[0].rm_so] = '\0'; + + char *prematch = strdup(copy); + if (!prematch) + { + free(match); + free(copy); + regfree(®); + free(decoded_text); + weechat_printf( + account->buffer, + _("%s%s: error allocating space for message"), + weechat_prefix("error"), WEECHAT_XMPP_PLUGIN_NAME); + return strdup(text); + } + free(copy); + + strncat(decoded_text, prematch, + MESSAGE_MAX_LENGTH - strlen(decoded_text) - 1); + free(prematch); + + char *replacement = message__translate_code(account, match); + free(match); + + strncat(decoded_text, replacement, + MESSAGE_MAX_LENGTH - strlen(decoded_text) - 1); + free(replacement); + } + strncat(decoded_text, cursor, + MESSAGE_MAX_LENGTH - strlen(decoded_text) - 1); + + message__htmldecode(decoded_text, decoded_text, + MESSAGE_MAX_LENGTH); + + regfree(®); + return decoded_text; +} diff --git a/message.h b/message.h new file mode 100644 index 0000000..fdf7263 --- /dev/null +++ b/message.h @@ -0,0 +1,13 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_MESSAGE_H_ +#define _WEECHAT_XMPP_MESSAGE_H_ + +#define MESSAGE_MAX_LENGTH 40000 + +char *message__decode(struct t_account *account, + const char *text); + +#endif /*WEECHAT_XMPP_MESSAGE_H*/ diff --git a/omemo.c b/omemo.c new file mode 100644 index 0000000..811a508 --- /dev/null +++ b/omemo.c @@ -0,0 +1,1133 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct t_omemo_db { + MDB_env *env; + MDB_dbi dbi_omemo; +}; + +#include "plugin.h" +#include "account.h" +#include "omemo.h" + +#define mdb_val_str(s) { \ + .mv_data = s, .mv_size = strlen(s), \ +} + +#define mdb_val_intptr(i) { \ + .mv_data = i, .mv_size = sizeof(*i), \ +} + +#define mdb_val_sizeof(t) { \ + .mv_data = NULL, .mv_size = sizeof(t), \ +} + +const char *OMEMO_ADVICE = "[OMEMO encrypted message (XEP-0384)]"; + +void signal_protocol_address_free(signal_protocol_address* ptr) { + if (!ptr) + return; + if (ptr->name) { + free((void*)ptr->name); + } + return free(ptr); +} + +void signal_protocol_address_set_name(signal_protocol_address* self, const char* name) { + if (!self) + return; + if (!name) + return; + char* n = malloc(strlen(name)+1); + memcpy(n, name, strlen(name)); + n[strlen(name)] = 0; + if (self->name) { + free((void*)self->name); + } + self->name = n; + self->name_len = strlen(n); +} + +char* signal_protocol_address_get_name(signal_protocol_address* self) { + if (!self) + return NULL; + if (!self->name) + return 0; + char* res = malloc(sizeof(char) * (self->name_len + 1)); + memcpy(res, self->name, self->name_len); + res[self->name_len] = 0; + return res; +} + +int32_t signal_protocol_address_get_device_id(signal_protocol_address* self) { + if (!self) + return -1; + return self->device_id; +} + +void signal_protocol_address_set_device_id(signal_protocol_address* self, int32_t device_id) { + if (!self) + return; + self->device_id = device_id; +} + +signal_protocol_address* signal_protocol_address_new(const char* name, int32_t device_id) { + if (!name) + return NULL; + signal_protocol_address* address = malloc(sizeof(signal_protocol_address)); + address->device_id = -1; + address->name = NULL; + signal_protocol_address_set_name(address, name); + signal_protocol_address_set_device_id(address, device_id); + return address; +} + +int aes_cipher(int cipher, size_t key_len, int* algo, int* mode) { + switch (key_len) { + case 16: + *algo = GCRY_CIPHER_AES128; + break; + case 24: + *algo = GCRY_CIPHER_AES192; + break; + case 32: + *algo = GCRY_CIPHER_AES256; + break; + default: + return SG_ERR_UNKNOWN; + } + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + *mode = GCRY_CIPHER_MODE_CBC; + break; + case SG_CIPHER_AES_CTR_NOPADDING: + *mode = GCRY_CIPHER_MODE_CTR; + break; + default: + return SG_ERR_UNKNOWN; + } + return SG_SUCCESS; +} + +void lock_function(void *user_data) +{ + (void) user_data; +} + +void unlock_function(void *user_data) +{ + (void) user_data; +} + +int cp_randomize(uint8_t *data, size_t len) { + gcry_randomize(data, len, GCRY_STRONG_RANDOM); + return SG_SUCCESS; +} + +int cp_random_generator(uint8_t *data, size_t len, void *user_data) { + (void) user_data; + + gcry_randomize(data, len, GCRY_STRONG_RANDOM); + return SG_SUCCESS; +} + +int cp_hmac_sha256_init(void **hmac_context, const uint8_t *key, size_t key_len, void *user_data) { + (void) user_data; + + gcry_mac_hd_t* ctx = malloc(sizeof(gcry_mac_hd_t)); + if (!ctx) return SG_ERR_NOMEM; + + if (gcry_mac_open(ctx, GCRY_MAC_HMAC_SHA256, 0, 0)) { + free(ctx); + return SG_ERR_UNKNOWN; + } + + if (gcry_mac_setkey(*ctx, key, key_len)) { + free(ctx); + return SG_ERR_UNKNOWN; + } + + *hmac_context = ctx; + + return SG_SUCCESS; +} + +int cp_hmac_sha256_update(void *hmac_context, const uint8_t *data, size_t data_len, void *user_data) { + (void) user_data; + + gcry_mac_hd_t* ctx = hmac_context; + + if (gcry_mac_write(*ctx, data, data_len)) return SG_ERR_UNKNOWN; + + return SG_SUCCESS; +} + +int cp_hmac_sha256_final(void *hmac_context, signal_buffer **output, void *user_data) { + (void) user_data; + + size_t len = gcry_mac_get_algo_maclen(GCRY_MAC_HMAC_SHA256); + uint8_t md[len]; + gcry_mac_hd_t* ctx = hmac_context; + + if (gcry_mac_read(*ctx, md, &len)) return SG_ERR_UNKNOWN; + + signal_buffer *output_buffer = signal_buffer_create(md, len); + if (!output_buffer) return SG_ERR_NOMEM; + + *output = output_buffer; + + return SG_SUCCESS; +} + +void cp_hmac_sha256_cleanup(void *hmac_context, void *user_data) { + (void) user_data; + + gcry_mac_hd_t* ctx = hmac_context; + if (ctx) { + gcry_mac_close(*ctx); + free(ctx); + } +} + +int cp_sha512_digest_init(void **digest_context, void *user_data) { + (void) user_data; + + gcry_md_hd_t* ctx = malloc(sizeof(gcry_mac_hd_t)); + if (!ctx) return SG_ERR_NOMEM; + + if (gcry_md_open(ctx, GCRY_MD_SHA512, 0)) { + free(ctx); + return SG_ERR_UNKNOWN; + } + + *digest_context = ctx; + + return SG_SUCCESS; +} + +int cp_sha512_digest_update(void *digest_context, const uint8_t *data, size_t data_len, void *user_data) { + (void) user_data; + + gcry_md_hd_t* ctx = digest_context; + + gcry_md_write(*ctx, data, data_len); + + return SG_SUCCESS; +} + +int cp_sha512_digest_final(void *digest_context, signal_buffer **output, void *user_data) { + (void) user_data; + + size_t len = gcry_md_get_algo_dlen(GCRY_MD_SHA512); + gcry_md_hd_t* ctx = digest_context; + + uint8_t* md = gcry_md_read(*ctx, GCRY_MD_SHA512); + if (!md) return SG_ERR_UNKNOWN; + + gcry_md_reset(*ctx); + + signal_buffer *output_buffer = signal_buffer_create(md, len); + free(md); + if (!output_buffer) return SG_ERR_NOMEM; + + *output = output_buffer; + + return SG_SUCCESS; +} + +void cp_sha512_digest_cleanup(void *digest_context, void *user_data) { + (void) user_data; + + gcry_md_hd_t* ctx = digest_context; + if (ctx) { + gcry_md_close(*ctx); + free(ctx); + } +} + +int cp_encrypt(signal_buffer **output, + int cipher, + const uint8_t *key, size_t key_len, + const uint8_t *iv, size_t iv_len, + const uint8_t *plaintext, size_t plaintext_len, + void *user_data) { + (void) user_data; + + int algo, mode, error_code = SG_ERR_UNKNOWN; + if (aes_cipher(cipher, key_len, &algo, &mode)) return SG_ERR_INVAL; + + gcry_cipher_hd_t ctx = {0}; + + if (gcry_cipher_open(&ctx, algo, mode, 0)) return SG_ERR_NOMEM; + + signal_buffer* padded = 0; + signal_buffer* out_buf = 0; + goto no_error; +error: + gcry_cipher_close(ctx); + if (padded != 0) { + signal_buffer_bzero_free(padded); + } + if (out_buf != 0) { + signal_buffer_free(out_buf); + } + return error_code; +no_error: + + if (gcry_cipher_setkey(ctx, key, key_len)) goto error; + + uint8_t tag_len = 0, pad_len = 0; + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + if (gcry_cipher_setiv(ctx, iv, iv_len)) goto error; + pad_len = 16 - (plaintext_len % 16); + if (pad_len == 0) pad_len = 16; + break; + case SG_CIPHER_AES_CTR_NOPADDING: + if (gcry_cipher_setctr(ctx, iv, iv_len)) goto error; + break; + default: + return SG_ERR_UNKNOWN; + } + + size_t padded_len = plaintext_len + pad_len; + padded = signal_buffer_alloc(padded_len); + if (padded == 0) { + error_code = SG_ERR_NOMEM; + goto error; + } + + memset(signal_buffer_data(padded) + plaintext_len, pad_len, pad_len); + memcpy(signal_buffer_data(padded), plaintext, plaintext_len); + + out_buf = signal_buffer_alloc(padded_len + tag_len); + if (out_buf == 0) { + error_code = SG_ERR_NOMEM; + goto error; + } + + if (gcry_cipher_encrypt(ctx, signal_buffer_data(out_buf), padded_len, signal_buffer_data(padded), padded_len)) goto error; + + if (tag_len > 0) { + if (gcry_cipher_gettag(ctx, signal_buffer_data(out_buf) + padded_len, tag_len)) goto error; + } + + *output = out_buf; + out_buf = 0; + + signal_buffer_bzero_free(padded); + padded = 0; + + gcry_cipher_close(ctx); + return SG_SUCCESS; +} + +int cp_decrypt(signal_buffer **output, + int cipher, + const uint8_t *key, size_t key_len, + const uint8_t *iv, size_t iv_len, + const uint8_t *ciphertext, size_t ciphertext_len, + void *user_data) { + (void) user_data; + + int algo, mode, error_code = SG_ERR_UNKNOWN; + *output = 0; + if (aes_cipher(cipher, key_len, &algo, &mode)) return SG_ERR_INVAL; + if (ciphertext_len == 0) return SG_ERR_INVAL; + + gcry_cipher_hd_t ctx = {0}; + + if (gcry_cipher_open(&ctx, algo, mode, 0)) return SG_ERR_NOMEM; + + signal_buffer* out_buf = 0; + goto no_error; +error: + gcry_cipher_close(ctx); + if (out_buf != 0) { + signal_buffer_bzero_free(out_buf); + } + return error_code; +no_error: + + if (gcry_cipher_setkey(ctx, key, key_len)) goto error; + + uint8_t tag_len = 0, pkcs_pad = 0; + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + if (gcry_cipher_setiv(ctx, iv, iv_len)) goto error; + pkcs_pad = 1; + break; + case SG_CIPHER_AES_CTR_NOPADDING: + if (gcry_cipher_setctr(ctx, iv, iv_len)) goto error; + break; + default: + goto error; + } + + size_t padded_len = ciphertext_len - tag_len; + out_buf = signal_buffer_alloc(padded_len); + if (out_buf == 0) { + error_code = SG_ERR_NOMEM; + goto error; + } + + if (gcry_cipher_decrypt(ctx, signal_buffer_data(out_buf), signal_buffer_len(out_buf), ciphertext, padded_len)) goto error; + + if (tag_len > 0) { + if (gcry_cipher_checktag(ctx, ciphertext + padded_len, tag_len)) goto error; + } + + if (pkcs_pad) { + uint8_t pad_len = signal_buffer_data(out_buf)[padded_len - 1]; + if (pad_len > 16 || pad_len > padded_len) goto error; + *output = signal_buffer_create(signal_buffer_data(out_buf), padded_len - pad_len); + signal_buffer_bzero_free(out_buf); + out_buf = 0; + } else { + *output = out_buf; + out_buf = 0; + } + + gcry_cipher_close(ctx); + return SG_SUCCESS; +} + +int iks_get_identity_key_pair(signal_buffer **public_data, signal_buffer **private_data, void *user_data) +{ + struct t_omemo *omemo = (struct t_omemo *)user_data; + MDB_txn *transaction; + MDB_val k_local_private_key = mdb_val_str("local_private_key"); + MDB_val k_local_public_key = mdb_val_str("local_public_key"); + MDB_val v_local_private_key, v_local_public_key; + + // Get the local client's identity key pair + if (mdb_txn_begin(omemo->db->env, NULL, MDB_RDONLY, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + } + + if (mdb_get(transaction, omemo->db->dbi_omemo, + &k_local_private_key, &v_local_private_key) && + mdb_get(transaction, omemo->db->dbi_omemo, + &k_local_public_key, &v_local_public_key)) + { + *private_data = signal_buffer_create(v_local_private_key.mv_data, v_local_private_key.mv_size); + *public_data = signal_buffer_create(v_local_public_key.mv_data, v_local_public_key.mv_size); + + if (mdb_txn_commit(transaction)) { + weechat_printf(NULL, "%sxmpp: failed to write lmdb transaction", + weechat_prefix("error")); + return -1; + }; + } + else + { + struct ratchet_identity_key_pair *identity; + + mdb_txn_abort(transaction); + + if (mdb_txn_begin(omemo->db->env, NULL, 0, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + return -1; + } + + signal_protocol_key_helper_generate_identity_key_pair( + &identity, omemo->context); + ec_private_key *private_key = ratchet_identity_key_pair_get_private(identity); + ec_public_key *public_key = ratchet_identity_key_pair_get_public(identity); + + ec_private_key_serialize(private_data, private_key); + ec_public_key_serialize(public_data, public_key); + + v_local_private_key.mv_data = signal_buffer_data(*private_data); + v_local_private_key.mv_size = signal_buffer_len(*private_data); + v_local_public_key.mv_data = signal_buffer_data(*public_data); + v_local_public_key.mv_size = signal_buffer_len(*public_data); + + if (mdb_put(transaction, omemo->db->dbi_omemo, + &k_local_private_key, &v_local_private_key, MDB_NOOVERWRITE) && + mdb_put(transaction, omemo->db->dbi_omemo, + &k_local_public_key, &v_local_public_key, MDB_NOOVERWRITE)) + { + weechat_printf(NULL, "%sxmpp: failed to write lmdb value", + weechat_prefix("error")); + return -1; + }; + + if (mdb_txn_commit(transaction)) { + weechat_printf(NULL, "%sxmpp: failed to write lmdb transaction", + weechat_prefix("error")); + return -1; + }; + } + + return 0; +} + +int iks_get_local_registration_id(void *user_data, uint32_t *registration_id) +{ + struct t_omemo *omemo = (struct t_omemo *)user_data; + MDB_txn *transaction; + MDB_val k_local_registration_id = mdb_val_str("local_registration_id"); + MDB_val v_local_registration_id = mdb_val_sizeof(uint32_t); + + // Return the local client's registration ID + if (mdb_txn_begin(omemo->db->env, NULL, MDB_RDONLY, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + return -1; + } + + if (mdb_get(transaction, omemo->db->dbi_omemo, + &k_local_registration_id, + &v_local_registration_id)) + { + *registration_id = *(uint32_t*)v_local_registration_id.mv_data; + + if (mdb_txn_commit(transaction)) { + weechat_printf(NULL, "%sxmpp: failed to write lmdb transaction", + weechat_prefix("error")); + return -1; + }; + } + else + { + mdb_txn_abort(transaction); + + if (mdb_txn_begin(omemo->db->env, NULL, 0, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + return -1; + } + + signal_protocol_key_helper_generate_registration_id( + (uint32_t*)&v_local_registration_id.mv_data, 0, omemo->context); + + if (mdb_put(transaction, omemo->db->dbi_omemo, + &k_local_registration_id, + &v_local_registration_id, MDB_NOOVERWRITE)) + { + weechat_printf(NULL, "%sxmpp: failed to write lmdb value", + weechat_prefix("error")); + return -1; + }; + + if (mdb_txn_commit(transaction)) { + weechat_printf(NULL, "%sxmpp: failed to write lmdb transaction", + weechat_prefix("error")); + return -1; + }; + } + + return 0; +} + +int iks_save_identity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *user_data) +{ + struct t_omemo *omemo = (struct t_omemo *)user_data; + MDB_txn *transaction; + MDB_val k_registration_id = { + .mv_data = NULL, + .mv_size = strlen("registration_id_") + address->name_len, + }; + MDB_val v_registration_id = mdb_val_intptr((uint32_t*)&address->device_id); + MDB_val k_identity_key = { + .mv_data = NULL, + .mv_size = strlen("identity_key_") + address->name_len, + }; + MDB_val v_identity_key = {.mv_data = key_data, .mv_size = key_len}; + + k_registration_id.mv_data = malloc(sizeof(char) * ( + k_registration_id.mv_size + 1)); + snprintf(k_registration_id.mv_data, k_registration_id.mv_size, + "registration_id_%s", address->name); + k_identity_key.mv_data = malloc(sizeof(char) * ( + k_identity_key.mv_size + 1)); + snprintf(k_identity_key.mv_data, k_identity_key.mv_size, + "identity_key_%s", address->name); + + // Save a remote client's identity key + if (mdb_txn_begin(omemo->db->env, NULL, 0, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + return -1; + } + + if (mdb_put(transaction, omemo->db->dbi_omemo, &k_registration_id, + &v_registration_id, 0) || + mdb_put(transaction, omemo->db->dbi_omemo, &k_identity_key, + &v_identity_key, 0)) { + weechat_printf(NULL, "%sxmpp: failed to write lmdb value", + weechat_prefix("error")); + return -1; + }; + + if (mdb_txn_commit(transaction)) { + weechat_printf(NULL, "%sxmpp: failed to write lmdb transaction", + weechat_prefix("error")); + return -1; + }; + + return 0; +} + +int iks_is_trusted_identity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *user_data) +{ + struct t_omemo *omemo = (struct t_omemo *)user_data; + MDB_txn *transaction; + MDB_val k_registration_id = { + .mv_data = NULL, + .mv_size = strlen("registration_id_") + address->name_len, + }; + MDB_val v_registration_id = mdb_val_intptr((uint32_t*)&address->device_id); + MDB_val k_identity_key = { + .mv_data = NULL, + .mv_size = strlen("identity_key_") + address->name_len, + }; + MDB_val v_identity_key = {.mv_data = key_data, .mv_size = key_len}; + int trusted = 1; + + k_registration_id.mv_data = malloc(sizeof(char) * ( + k_registration_id.mv_size + 1)); + snprintf(k_registration_id.mv_data, k_registration_id.mv_size, + "registration_id_%s", address->name); + k_identity_key.mv_data = malloc(sizeof(char) * ( + k_identity_key.mv_size + 1)); + snprintf(k_identity_key.mv_data, k_identity_key.mv_size, + "identity_key_%s", address->name); + + // Verify a remote client's identity key + if (mdb_txn_begin(omemo->db->env, NULL, MDB_RDONLY, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + return -1; + } + + if (mdb_get(transaction, omemo->db->dbi_omemo, &k_registration_id, + &v_registration_id) || + mdb_get(transaction, omemo->db->dbi_omemo, &k_identity_key, + &v_identity_key)) { + weechat_printf(NULL, "%sxmpp: failed to write lmdb value", + weechat_prefix("error")); + return -1; + }; + + if (*(uint32_t*)v_registration_id.mv_data != (uint32_t)address->device_id) + trusted = 0; + if (v_identity_key.mv_size != key_len || + memcmp(v_identity_key.mv_data, key_data, key_len) != 0) + trusted = 0; + + if (mdb_txn_commit(transaction)) { + weechat_printf(NULL, "%sxmpp: failed to write lmdb transaction", + weechat_prefix("error")); + return -1; + }; + + return trusted; +} + +void iks_destroy_func(void *user_data) +{ + struct t_omemo *omemo = (struct t_omemo *)user_data; + (void) omemo; + // Function called to perform cleanup when the data store context is being destroyed +} + +int pks_load_pre_key(signal_buffer **record, uint32_t pre_key_id, void *user_data) +{ + struct t_omemo *omemo = (struct t_omemo *)user_data; + MDB_txn *transaction; + MDB_val k_pre_key = mdb_val_str("pre_key"); + MDB_val v_pre_key; + + if (mdb_txn_begin(omemo->db->env, NULL, MDB_RDONLY, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + } + + if (mdb_get(transaction, omemo->db->dbi_omemo, + &k_pre_key, &v_pre_key)) + { + *record = signal_buffer_create(v_pre_key.mv_data, v_pre_key.mv_size); + + if (mdb_txn_commit(transaction)) { + weechat_printf(NULL, "%sxmpp: failed to close lmdb transaction", + weechat_prefix("error")); + return -1; + }; + } + else + { + signal_protocol_key_helper_pre_key_list_node *pre_keys_list; + session_pre_key *pre_key = NULL; + + mdb_txn_abort(transaction); + + /* + signal_protocol_key_helper_generate_pre_keys( + &pre_keys_list, 0, 100, omemo->context); + pre_key = signal_protocol_key_helper_key_list_element(pre_keys_list); + signal_protocol_key_helper_key_list_next(pre_keys_list); + + uint32_t id = session_pre_key_get_id(pre_key); + session_pre_key_serialize(&record, pre_key); + + signal_protocol_key_helper_key_list_free(pre_keys_list); + + if (mdb_txn_begin(omemo->db->env, NULL, 0, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + return -1; + } + + v_pre_key.mv_data = signal_buffer_data(*record); + v_pre_key.mv_size = signal_buffer_len(*record); + + if (mdb_put(transaction, omemo->db->dbi_omemo, + &k_pre_key, &v_pre_key, MDB_NOOVERWRITE)) + { + weechat_printf(NULL, "%sxmpp: failed to write lmdb value", + weechat_prefix("error")); + return -1; + }; + + if (mdb_txn_commit(transaction)) { + weechat_printf(NULL, "%sxmpp: failed to write lmdb transaction", + weechat_prefix("error")); + return -1; + }; + */ + return -1; + } + + return 0; +} + +int pks_store_pre_key(uint32_t pre_key_id, uint8_t *record, size_t record_len, void *user_data) +{ + (void) pre_key_id; + (void) record; + (void) record_len; + (void) user_data; + return -1; + struct t_omemo *omemo = (struct t_omemo *)user_data; + MDB_txn *transaction; + MDB_val k_pre_key = mdb_val_str("pre_key"); + MDB_val v_pre_key; + + if (mdb_txn_begin(omemo->db->env, NULL, MDB_RDONLY, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + } + + if (mdb_get(transaction, omemo->db->dbi_omemo, + &k_pre_key, &v_pre_key)) + { + *record = signal_buffer_create(v_pre_key.mv_data, v_pre_key.mv_size); + + if (mdb_txn_commit(transaction)) { + weechat_printf(NULL, "%sxmpp: failed to close lmdb transaction", + weechat_prefix("error")); + return -1; + }; + } + else + { + signal_protocol_key_helper_pre_key_list_node *pre_keys_list; + session_pre_key *pre_key = NULL; + + mdb_txn_abort(transaction); + + /* + signal_protocol_key_helper_generate_pre_keys( + &pre_keys_list, 0, 100, omemo->context); + pre_key = signal_protocol_key_helper_key_list_element(pre_keys_list); + signal_protocol_key_helper_key_list_next(pre_keys_list); + + uint32_t id = session_pre_key_get_id(pre_key); + session_pre_key_serialize(&record, pre_key); + + signal_protocol_key_helper_key_list_free(pre_keys_list); + + if (mdb_txn_begin(omemo->db->env, NULL, 0, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + return -1; + } + + v_pre_key.mv_data = signal_buffer_data(*record); + v_pre_key.mv_size = signal_buffer_len(*record); + + if (mdb_put(transaction, omemo->db->dbi_omemo, + &k_pre_key, &v_pre_key, MDB_NOOVERWRITE)) + { + weechat_printf(NULL, "%sxmpp: failed to write lmdb value", + weechat_prefix("error")); + return -1; + }; + + if (mdb_txn_commit(transaction)) { + weechat_printf(NULL, "%sxmpp: failed to write lmdb transaction", + weechat_prefix("error")); + return -1; + }; + */ + return -1; + } + + return 0; +} + +int pks_contains_pre_key(uint32_t pre_key_id, void *user_data) +{ + (void) pre_key_id; + (void) user_data; + return -1; +} + +int pks_remove_pre_key(uint32_t pre_key_id, void *user_data) +{ + (void) pre_key_id; + (void) user_data; + return -1; +} + +void pks_destroy_func(void *user_data) +{ + (void) user_data; +} + +int spks_load_signed_pre_key(signal_buffer **record, uint32_t signed_pre_key_id, void *user_data) +{ + (void) record; + (void) signed_pre_key_id; + (void) user_data; + return -1; + //session_signed_pre_key *signed_pre_key; + //int start_id = 0; + //time_t timestamp = time(NULL); + //signal_protocol_key_helper_generate_signed_pre_key(&signed_pre_key, new_omemo->identity, 5, timestamp, new_omemo->context); +} + +int spks_store_signed_pre_key(uint32_t signed_pre_key_id, uint8_t *record, size_t record_len, void *user_data) +{ + (void) signed_pre_key_id; + (void) record; + (void) record_len; + (void) user_data; + return -1; +} + +int spks_contains_signed_pre_key(uint32_t signed_pre_key_id, void *user_data) +{ + (void) signed_pre_key_id; + (void) user_data; + return -1; +} + +int spks_remove_signed_pre_key(uint32_t signed_pre_key_id, void *user_data) +{ + (void) signed_pre_key_id; + (void) user_data; + return -1; +} + +void spks_destroy_func(void *user_data) +{ + (void) user_data; +} + +int ss_load_session_func(signal_buffer **record, signal_buffer **user_record, const signal_protocol_address *address, void *user_data) +{ + (void) record; + (void) user_record; + (void) address; + (void) user_data; + return -1; +} + +int ss_get_sub_device_sessions_func(signal_int_list **sessions, const char *name, size_t name_len, void *user_data) +{ + (void) sessions; + (void) name; + (void) name_len; + (void) user_data; + return -1; +} + +int ss_store_session_func(const signal_protocol_address *address, uint8_t *record, size_t record_len, uint8_t *user_record, size_t user_record_len, void *user_data) +{ + (void) address; + (void) record; + (void) record_len; + (void) user_record; + (void) user_record_len; + (void) user_data; + return -1; +} + +int ss_contains_session_func(const signal_protocol_address *address, void *user_data) +{ + (void) address; + (void) user_data; + return -1; +} + +int ss_delete_session_func(const signal_protocol_address *address, void *user_data) +{ + (void) address; + (void) user_data; + return -1; +} + +int ss_delete_all_sessions_func(const char *name, size_t name_len, void *user_data) +{ + (void) name; + (void) name_len; + (void) user_data; + return -1; +} + +void ss_destroy_func(void *user_data) +{ + (void) user_data; +} + +int sks_store_sender_key(const signal_protocol_sender_key_name *sender_key_name, uint8_t *record, size_t record_len, uint8_t *user_record, size_t user_record_len, void *user_data) +{ + (void) sender_key_name; + (void) record; + (void) record_len; + (void) user_record; + (void) user_record_len; + (void) user_data; + return -1; +} + +int sks_load_sender_key(signal_buffer **record, signal_buffer **user_record, const signal_protocol_sender_key_name *sender_key_name, void *user_data) +{ + (void) record; + (void) user_record; + (void) sender_key_name; + (void) user_data; + return -1; +} + +void sks_destroy_func(void *user_data) +{ + (void) user_data; +} + +void omemo__log_emit_weechat(int level, const char *message, size_t len, void *user_data) +{ + struct t_gui_buffer *buffer = (struct t_gui_buffer*)user_data; + + static const char *log_level_name[5] = {"error", "warn", "notice", "info", "debug"}; + + const char *tags = level < SG_LOG_DEBUG ? "no_log" : NULL; + + weechat_printf_date_tags( + buffer, 0, tags, + _("%somemo (%s): %.*s"), + weechat_prefix("network"), + log_level_name[level], len, message); +} + +void omemo__init(struct t_gui_buffer *buffer, struct t_omemo **omemo, + const char *account_name) +{ + struct t_omemo *new_omemo; + + gcry_check_version(NULL); + + new_omemo = calloc(1, sizeof(**omemo)); + + new_omemo->db = malloc(sizeof(struct t_omemo_db)); + + signal_context_create(&new_omemo->context, buffer); + signal_context_set_log_function(new_omemo->context, &omemo__log_emit_weechat); + + mdb_env_create(&new_omemo->db->env); + mdb_env_set_maxdbs(new_omemo->db->env, 50); + mdb_env_set_mapsize(new_omemo->db->env, (size_t)1048576 * 100000); // 1MB * 100000 + char *path = weechat_string_eval_expression("${weechat_data_dir}/xmpp.omemo.db", + NULL, NULL, NULL); + if (mdb_env_open(new_omemo->db->env, path, MDB_NOSUBDIR, 0664) != 0) + { + return; + } + free(path); + + MDB_txn *parentTransaction = NULL; + MDB_txn *transaction; + if (mdb_txn_begin(new_omemo->db->env, parentTransaction, 0 ? MDB_RDONLY : 0, &transaction)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb transaction", + weechat_prefix("error")); + } + + size_t db_name_len = strlen("omemo_") + strlen(account_name); + char *db_name = malloc(sizeof(char) * (db_name_len + 1)); + snprintf(db_name, db_name_len+1, "identity_key_%s", account_name); + if (mdb_dbi_open(transaction, db_name, MDB_DUPSORT | MDB_CREATE, &new_omemo->db->dbi_omemo)) { + weechat_printf(NULL, "%sxmpp: failed to open lmdb database", + weechat_prefix("error")); + } + + mdb_txn_abort(transaction); + + struct signal_crypto_provider crypto_provider = { + .random_func = &cp_random_generator, + .hmac_sha256_init_func = &cp_hmac_sha256_init, + .hmac_sha256_update_func = &cp_hmac_sha256_update, + .hmac_sha256_final_func = &cp_hmac_sha256_final, + .hmac_sha256_cleanup_func = &cp_hmac_sha256_cleanup, + .sha512_digest_init_func = &cp_sha512_digest_init, + .sha512_digest_update_func = &cp_sha512_digest_update, + .sha512_digest_final_func = &cp_sha512_digest_final, + .sha512_digest_cleanup_func = &cp_sha512_digest_cleanup, + .encrypt_func = &cp_encrypt, + .decrypt_func = &cp_decrypt, + .user_data = new_omemo, + }; + + signal_context_set_crypto_provider(new_omemo->context, &crypto_provider); + signal_context_set_locking_functions(new_omemo->context, &lock_function, &unlock_function); + + signal_protocol_store_context_create(&new_omemo->store_context, new_omemo->context); + + struct signal_protocol_identity_key_store identity_key_store = { + .get_identity_key_pair = &iks_get_identity_key_pair, + .get_local_registration_id = &iks_get_local_registration_id, + .save_identity = &iks_save_identity, + .is_trusted_identity = &iks_is_trusted_identity, + .destroy_func = &iks_destroy_func, + .user_data = new_omemo, + }; + + signal_protocol_store_context_set_identity_key_store( + new_omemo->store_context, &identity_key_store); + + struct signal_protocol_pre_key_store pre_key_store = { + .load_pre_key = &pks_load_pre_key, + .store_pre_key = &pks_store_pre_key, + .contains_pre_key = &pks_contains_pre_key, + .remove_pre_key = &pks_remove_pre_key, + .destroy_func = &pks_destroy_func, + .user_data = new_omemo, + }; + + signal_protocol_store_context_set_pre_key_store( + new_omemo->store_context, &pre_key_store); + + struct signal_protocol_signed_pre_key_store signed_pre_key_store = { + .load_signed_pre_key = &spks_load_signed_pre_key, + .store_signed_pre_key = &spks_store_signed_pre_key, + .contains_signed_pre_key = &spks_contains_signed_pre_key, + .remove_signed_pre_key = &spks_remove_signed_pre_key, + .destroy_func = &spks_destroy_func, + .user_data = new_omemo, + }; + + signal_protocol_store_context_set_signed_pre_key_store( + new_omemo->store_context, &signed_pre_key_store); + + struct signal_protocol_session_store session_store = { + .load_session_func = &ss_load_session_func, + .get_sub_device_sessions_func = &ss_get_sub_device_sessions_func, + .store_session_func = &ss_store_session_func, + .contains_session_func = &ss_contains_session_func, + .delete_session_func = &ss_delete_session_func, + .delete_all_sessions_func = &ss_delete_all_sessions_func, + .destroy_func = &ss_destroy_func, + .user_data = new_omemo, + }; + + signal_protocol_store_context_set_session_store( + new_omemo->store_context, &session_store); + + struct signal_protocol_sender_key_store sender_key_store = { + .store_sender_key = &sks_store_sender_key, + .load_sender_key = &sks_load_sender_key, + .destroy_func = &sks_destroy_func, + .user_data = new_omemo, + }; + + signal_protocol_store_context_set_sender_key_store( + new_omemo->store_context, &sender_key_store); + + *omemo = new_omemo; +} + +void omemo__serialize(struct t_omemo *omemo, char **device, + char **identity, size_t *identity_len) +{ + if (device) + { + size_t id_slen = log10(omemo->device_id) * 2; + char *id = malloc(sizeof(char) * id_slen); + snprintf(id, id_slen, "%d", omemo->device_id); + + *device = id; + } + if (identity) + { + signal_buffer *buffer; + ratchet_identity_key_pair_serialize(&buffer, omemo->identity); + + size_t key_slen = signal_buffer_len(buffer) * 2; + char *key = malloc(sizeof(char) * key_slen); + size_t length = weechat_string_base_encode(64, (char*)signal_buffer_data(buffer), + signal_buffer_len(buffer), key); + + *identity = key; + if (identity_len) + *identity_len = length; + } +} + +void omemo__deserialize(struct t_omemo *omemo, const char *device, + const char *identity, size_t identity_len) +{ + if (device) + { + uint32_t id = device[0] ? atoi(device) : 0; + + omemo->device_id = id; + } + if (identity) + { + uint8_t *key = malloc(sizeof(uint8_t) * identity_len); + size_t length = weechat_string_base_decode(64, identity, (char*)key); + + ratchet_identity_key_pair_deserialize(&omemo->identity, + key, length, omemo->context); + } +} + +void omemo__free(struct t_omemo *omemo) +{ + if (omemo) + { + if (omemo->context) + signal_context_destroy(omemo->context); + if (omemo->identity) + ratchet_identity_key_pair_destroy( + (signal_type_base *)omemo->identity); + free(omemo); + } +} diff --git a/omemo.h b/omemo.h new file mode 100644 index 0000000..03c29b2 --- /dev/null +++ b/omemo.h @@ -0,0 +1,33 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_OMEMO_H_ +#define _WEECHAT_XMPP_OMEMO_H_ + +extern const char *OMEMO_ADVICE; + +struct t_omemo +{ + struct signal_context *context; + struct signal_protocol_store_context *store_context; + + struct t_omemo_db *db; + + struct ratchet_identity_key_pair *identity; + + uint32_t device_id; +}; + +void omemo__init(struct t_gui_buffer *buffer, struct t_omemo **omemo, + const char *account_name); + +void omemo__serialize(struct t_omemo *omemo, char **device, + char **identity, size_t *identity_len); + +void omemo__deserialize(struct t_omemo *omemo, const char *device, + const char *identity, size_t identity_len); + +void omemo__free(struct t_omemo *omemo); + +#endif /*WEECHAT_XMPP_OMEMO_H*/ diff --git a/pgp.c b/pgp.c new file mode 100644 index 0000000..427bf85 --- /dev/null +++ b/pgp.c @@ -0,0 +1,388 @@ +// This Source Code Form is subject to the terms of the Mozilla PublicAA +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include + +#include "plugin.h" +#include "pgp.h" + +#define RNP_SUCCESS 0 + +#define PGP_MESSAGE_HEADER "-----BEGIN PGP MESSAGE-----\r\n\r\n" +#define PGP_MESSAGE_FOOTER "\r\n-----END PGP MESSAGE-----\r\n" +#define PGP_SIGNATURE_HEADER "-----BEGIN PGP SIGNATURE-----\r\n\r\n" +#define PGP_SIGNATURE_FOOTER "\r\n-----END PGP SIGNATURE-----\r\n" + +const char *PGP_ADVICE = "[PGP encrypted message (XEP-0027)]"; + +void pgp__init(struct t_pgp **pgp, const char *pub, const char *sec) +{ + struct t_pgp *new_pgp; + rnp_input_t keyring; + + new_pgp = calloc(1, sizeof(**pgp)); + + if (rnp_ffi_create(&new_pgp->context, + RNP_KEYSTORE_GPG, RNP_KEYSTORE_GPG) != RNP_SUCCESS) { + return; + } + + if (rnp_input_from_path(&keyring, pub) == RNP_SUCCESS) { + if (rnp_load_keys(new_pgp->context, RNP_KEYSTORE_GPG, + keyring, RNP_LOAD_SAVE_PUBLIC_KEYS) == RNP_SUCCESS) { + rnp_input_destroy(keyring); + } + } + + if (rnp_input_from_path(&keyring, sec) == RNP_SUCCESS) { + if (rnp_load_keys(new_pgp->context, RNP_KEYSTORE_GPG, + keyring, RNP_LOAD_SAVE_SECRET_KEYS) == RNP_SUCCESS) { + rnp_input_destroy(keyring); + } + } + + *pgp = new_pgp; +} + +void pgp__free(struct t_pgp *pgp) +{ + if (pgp) + { + if (pgp->context) + free(pgp->context); + free(pgp); + } +} + +char *pgp__encrypt(struct t_gui_buffer *buffer, struct t_pgp *pgp, const char *source, const char *target, const char *message) +{ + rnp_op_encrypt_t encrypt = NULL; + rnp_key_handle_t key = NULL; + rnp_input_t keyfile = NULL; + rnp_input_t input = NULL; + rnp_output_t output = NULL; + char * result = NULL; + + rnp_result_t ret; + + /* create memory input and file output objects for the message and encrypted message */ + if ((ret = rnp_input_from_memory(&input, (uint8_t *)message, strlen(message), false)) != + RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to create input object: %s\n", weechat_prefix("error"), reason); + goto encrypt_finish; + } + + if ((ret = rnp_output_to_memory(&output, 0)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to create output object: %s\n", weechat_prefix("error"), reason); + goto encrypt_finish; + } + + /* create encryption operation */ + if ((ret = rnp_op_encrypt_create(&encrypt, pgp->context, input, output)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to create encrypt operation: %s\n", weechat_prefix("error"), reason); + goto encrypt_finish; + } + + /* setup encryption parameters */ + rnp_op_encrypt_set_armor(encrypt, true); + rnp_op_encrypt_set_file_name(encrypt, "message.txt"); + rnp_op_encrypt_set_file_mtime(encrypt, time(NULL)); + rnp_op_encrypt_set_compression(encrypt, "ZIP", 6); + rnp_op_encrypt_set_cipher(encrypt, RNP_ALGNAME_AES_256); + rnp_op_encrypt_set_aead(encrypt, "None"); + + /* locate recipient's key and add it to the operation context. */ + if ((ret = rnp_locate_key(pgp->context, "keyid", target, &key)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to locate recipient key: %s\n", weechat_prefix("error"), reason); + goto encrypt_finish; + } + + if ((ret = rnp_op_encrypt_add_recipient(encrypt, key)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to add recipient: %s\n", weechat_prefix("error"), reason); + goto encrypt_finish; + } + rnp_key_handle_destroy(key); + key = NULL; + + /* locate carbon-copy key and add it to the operation context. */ + if ((ret = rnp_locate_key(pgp->context, "keyid", source, &key)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to locate recipient key: %s\n", weechat_prefix("error"), reason); + goto encrypt_finish; + } + + if ((ret = rnp_op_encrypt_add_recipient(encrypt, key)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to add recipient: %s\n", weechat_prefix("error"), reason); + goto encrypt_finish; + } + rnp_key_handle_destroy(key); + key = NULL; + + /* execute encryption operation */ + if ((ret = rnp_op_encrypt_execute(encrypt)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: encryption failed: %s\n", weechat_prefix("error"), reason); + goto encrypt_finish; + } + + uint8_t *buf; + size_t buf_len; + + rnp_output_memory_get_buf(output, &buf, &buf_len, false); + result = strndup((char *)buf + strlen(PGP_MESSAGE_HEADER), + buf_len - strlen(PGP_MESSAGE_HEADER) - strlen(PGP_MESSAGE_FOOTER)); +encrypt_finish: + rnp_op_encrypt_destroy(encrypt); + rnp_input_destroy(keyfile); + rnp_input_destroy(input); + rnp_output_destroy(output); + rnp_key_handle_destroy(key); + return result; +} + +//"hQIMAzlgcSFDGLKEAQ//cGG3DFughC5xBF7xeXz1RdayOfhBAPfoZIq62MVuSnfS\nMfig65Zxz1LtAnnFq90TZY7hiHPBtVlYqg47AbSoYweMdpXsKgbUrd3NNf6k2nsZ\nUkChCtyGuHi8pTzclfle7gT0nNXJ1WcLCZ4ORZCrg3D5A+YTO9tdmE8GQsTT6TdV\nbbxF5yR4JF5SzFhuFL3ZoXPXrWylcwKXarYfoOTa6M2vSsCwApVIXQgJ/FI46sLT\nb0B/EVCjFvcvjkNr7+K7mQtth+x0a0pC4BtEhRvnIRAe/sdGp8NY+DP76clx4U+k\nIDG4H92F632pR6eEIoZttnBoaj0O4sTVAJCao5AoecR4w2FDqBWWtIyQp5vbo17/\nMtzungkk5vQP6Jhu36wa+JKpbHoxomVpHPZfAtIoyaY6pzQ0bUomIlSVpbZDvF68\nZKTlFd89Pm5x0JO5gsVYvf+N9Ed33d34n/0CFz5K5Tgu4Bk0v4LWEy3wtNsuQB4p\nkBSZJk7I2BakcRwP0zwld6rRHFIX1pb7zqThBPZGB9RkWPltiktUTibOII12tWhi\nksFpQJ8l1A8h9vM5kUXIeD6H2yP0CBUEIZF3Sf+jiSRZ/1/n3KoUrKEzkf/y4xgv\n1LA4pMjNLEr6J2fqGyYRFv4Bxv3PIvF17V5CwOtguxGRJHJXdIzm1BSHSqXxHezS\nYAFXMUb9fw3QX7Ed23KiyZjzd/LRsQBqMs9RsYyZB2PqF9x84lQYYbE8lErrryvK\nUEtmJKPw3Hvb7kgGox5vl5+KCg9q64EU9TgQpufYNShKtDz7Fsvc+ncgZoshDUeo\npw==\n=euIB" +char *pgp__decrypt(struct t_gui_buffer *buffer, struct t_pgp *pgp, const char *ciphertext) +{ + rnp_input_t input = NULL; + rnp_output_t output = NULL; + uint8_t * buf = NULL; + size_t buf_len = 0; + char * result = NULL; + + rnp_result_t ret; + + buf_len = strlen(PGP_MESSAGE_HEADER) + strlen(ciphertext) + strlen(PGP_MESSAGE_FOOTER) + 1; + buf = malloc(sizeof(char) * buf_len); + buf_len = snprintf((char *)buf, buf_len, PGP_MESSAGE_HEADER "%s" PGP_MESSAGE_FOOTER, ciphertext); + + /* create file input and memory output objects for the encrypted message and decrypted + * message */ + if ((ret = rnp_input_from_memory(&input, buf, buf_len, false)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to create input object: %s\n", weechat_prefix("error"), reason); + goto decrypt_finish; + } + + if ((ret = rnp_output_to_memory(&output, 0)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to create output object: %s\n", weechat_prefix("error"), reason); + goto decrypt_finish; + } + + if ((ret = rnp_decrypt(pgp->context, input, output)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: public-key decryption failed: %s\n", weechat_prefix("error"), reason); + goto decrypt_finish; + } + free(buf); + + /* get the decrypted message from the output structure */ + if (rnp_output_memory_get_buf(output, &buf, &buf_len, false) != RNP_SUCCESS) { + goto decrypt_finish; + } + + result = strndup((const char *)buf, (int)buf_len); +decrypt_finish: + rnp_input_destroy(input); + rnp_output_destroy(output); + return result; +} + +char *pgp__verify(struct t_gui_buffer *buffer, struct t_pgp *pgp, const char *certificate) +{ + rnp_op_verify_t verify = NULL; + rnp_input_t input = NULL; + rnp_input_t signature = NULL; + uint8_t * buf = NULL; + size_t buf_len = 0; + size_t sigcount = 0; + char * result = NULL; + + rnp_result_t ret; + + buf_len = strlen(PGP_SIGNATURE_HEADER) + strlen(certificate) + strlen(PGP_SIGNATURE_FOOTER) + 1; + buf = malloc(sizeof(char) * buf_len); + buf_len = snprintf((char *)buf, buf_len, PGP_SIGNATURE_HEADER "%s" PGP_SIGNATURE_FOOTER, certificate); + + /* create file input memory objects for the signed message and verified message */ + if ((ret = rnp_input_from_memory(&input, buf, buf_len, false)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to create input object: %s\n", weechat_prefix("error"), reason); + goto verify_finish; + } + + if ((ret = rnp_input_from_memory(&signature, buf, buf_len, false)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to create input object: %s\n", weechat_prefix("error"), reason); + goto verify_finish; + } + + if ((ret = rnp_op_verify_detached_create(&verify, pgp->context, input, signature)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to create verification context: %s\n", weechat_prefix("error"), reason); + goto verify_finish; + } + + //if (( + ret = rnp_op_verify_execute(verify) + ; + // ) != RNP_ERROR_SIGNATURE_INVALID) + // if (ret != RNP_ERROR_SIGNATURE_INVALID) { + // const char *reason = rnp_result_to_string(ret); + // weechat_printf(buffer, "%spgp: failed to execute verification operation: %s\n", weechat_prefix("error"), reason); + // goto verify_finish; + // } + + /* now check signatures and get some info about them */ + if ((ret = rnp_op_verify_get_signature_count(verify, &sigcount)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to get signature count: %s\n", weechat_prefix("error"), reason); + goto verify_finish; + } + + for (size_t i = 0; i < sigcount; i++) { + rnp_op_verify_signature_t sig = NULL; + rnp_key_handle_t key = NULL; + rnp_signature_handle_t signature = NULL; + char * keyid = NULL; + + if ((ret = rnp_op_verify_get_signature_at(verify, i, &sig)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to get signature %d: %s\n", weechat_prefix("error"), (int)i, reason); + goto verify_finish; + } + + if ((ret = rnp_op_verify_signature_get_key(sig, &key)) == RNP_SUCCESS) { + if ((ret = rnp_key_get_keyid(key, &keyid)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to get key id %d: %s\n", weechat_prefix("error"), (int)i, reason); + rnp_key_handle_destroy(key); + goto verify_finish; + } + + if ((ret = rnp_key_get_signature_at(key, 0, &signature)) == RNP_SUCCESS) { + if ((ret = rnp_signature_get_keyid(signature, &keyid)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to get key id: %s\n", weechat_prefix("error"), reason); + rnp_key_handle_destroy(key); + goto verify_finish; + } + rnp_signature_handle_destroy(signature); + } + } else { + if ((ret = rnp_op_verify_signature_get_handle(sig, &signature)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to get signature's %d handle: %s\n", weechat_prefix("error"), (int)i, reason); + goto verify_finish; + } + + if ((ret = rnp_signature_get_keyid(signature, &keyid)) != RNP_SUCCESS) { + const char *reason = rnp_result_to_string(ret); + weechat_printf(buffer, "%spgp: failed to get key id: %s\n", weechat_prefix("error"), reason); + rnp_key_handle_destroy(key); + goto verify_finish; + } + } + + result = strdup(keyid); + rnp_buffer_destroy(keyid); + rnp_key_handle_destroy(key); + break; + } + +verify_finish: + rnp_op_verify_destroy(verify); + rnp_input_destroy(input); + rnp_input_destroy(signature); + return result; +} + +char *pgp__sign(struct t_gui_buffer *buffer, struct t_pgp *pgp, const char *source, const char *message) +{ + rnp_input_t keyfile = NULL; + rnp_input_t input = NULL; + rnp_output_t output = NULL; + rnp_op_sign_t sign = NULL; + rnp_key_handle_t key = NULL; + uint8_t * buf = NULL; + size_t buf_len = 0; + char * result = NULL; + + /* create file input and memory output objects for the encrypted message and decrypted + * message */ + if (rnp_input_from_memory(&input, (uint8_t *)message, strlen(message), false) != + RNP_SUCCESS) { + weechat_printf(buffer, "%spgp: failed to create input object\n", weechat_prefix("error")); + goto sign_finish; + } + + if (rnp_output_to_memory(&output, 0) != RNP_SUCCESS) { + weechat_printf(buffer, "%spgp: failed to create output object\n", weechat_prefix("error")); + goto sign_finish; + } + + /* initialize and configure sign operation */ + if (rnp_op_sign_detached_create(&sign, pgp->context, input, output) != RNP_SUCCESS) { + weechat_printf(buffer, "%spgp: failed to create sign operation\n", weechat_prefix("error")); + goto sign_finish; + } + + /* armor, file name, compression */ + rnp_op_sign_set_armor(sign, true); + rnp_op_sign_set_file_name(sign, "message.txt"); + rnp_op_sign_set_file_mtime(sign, time(NULL)); + rnp_op_sign_set_compression(sign, "ZIP", 6); + /* signatures creation time - by default will be set to the current time as well */ + rnp_op_sign_set_creation_time(sign, time(NULL)); + /* signatures expiration time - by default will be 0, i.e. never expire */ + rnp_op_sign_set_expiration_time(sign, 365 * 24 * 60 * 60); + /* set hash algorithm - should be compatible for all signatures */ + rnp_op_sign_set_hash(sign, RNP_ALGNAME_SHA256); + + /* now add signatures. First locate the signing key, then add and setup signature */ + if (rnp_locate_key(pgp->context, "keyid", source, &key) != RNP_SUCCESS) { + weechat_printf(buffer, "%spgp: failed to locate signing key: %s\n", weechat_prefix("error"), source); + goto sign_finish; + } + + if (rnp_op_sign_add_signature(sign, key, NULL) != RNP_SUCCESS) { + weechat_printf(buffer, "%spgp: failed to add signature for key: %s\n", weechat_prefix("error"), source); + goto sign_finish; + } + + rnp_key_handle_destroy(key); + key = NULL; + + /* finally do signing */ + if (rnp_op_sign_execute(sign) != RNP_SUCCESS) { + weechat_printf(buffer, "%spgp: failed to sign with key: %s\n", weechat_prefix("error"), source); + goto sign_finish; + } + + /* get the signature from the output structure */ + if (rnp_output_memory_get_buf(output, &buf, &buf_len, false) != RNP_SUCCESS) { + goto sign_finish; + } + + result = strndup((char *)buf + strlen(PGP_SIGNATURE_HEADER), + buf_len - strlen(PGP_SIGNATURE_HEADER) - strlen(PGP_SIGNATURE_FOOTER)); +sign_finish: + rnp_input_destroy(keyfile); + rnp_key_handle_destroy(key); + rnp_op_sign_destroy(sign); + rnp_input_destroy(input); + rnp_output_destroy(output); + return result; +} diff --git a/pgp.h b/pgp.h new file mode 100644 index 0000000..0e12e6a --- /dev/null +++ b/pgp.h @@ -0,0 +1,28 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_PGP_H_ +#define _WEECHAT_XMPP_PGP_H_ + +extern const char *PGP_ADVICE; + +struct t_pgp +{ + struct rnp_ffi_st *context; + const char *keyid; +}; + +void pgp__init(struct t_pgp **pgp, const char *pub, const char *sec); + +void pgp__free(struct t_pgp *pgp); + +char *pgp__decrypt(struct t_gui_buffer *buffer, struct t_pgp *pgp, const char *ciphertext); + +char *pgp__encrypt(struct t_gui_buffer *buffer, struct t_pgp *pgp, const char *source, const char *target, const char *message); + +char *pgp__verify(struct t_gui_buffer *buffer, struct t_pgp *pgp, const char *certificate); + +char *pgp__sign(struct t_gui_buffer *buffer, struct t_pgp *pgp, const char *source, const char *message); + +#endif /*WEECHAT_XMPP_PGP_H*/ diff --git a/plugin.cpp b/plugin.cpp index c1b70c7..e6d6d96 100644 --- a/plugin.cpp +++ b/plugin.cpp @@ -14,6 +14,7 @@ namespace c { #include #include +#include "plugin.h" #include "config.h" #include "account.h" #include "connection.h" @@ -21,6 +22,16 @@ namespace c { #include "input.h" #include "buffer.h" #include "completion.h" + + struct t_weechat_plugin *weechat_xmpp_plugin() { + return weechat_plugin; + }; + const char *weechat_xmpp_plugin_name() { + return weechat::plugin::instance.name().data(); + }; + const char *weechat_xmpp_plugin_version() { + return weechat::plugin::instance.version().data(); + }; } #define TIMER_INTERVAL_SEC 0.01 @@ -99,11 +110,11 @@ namespace weechat { bool plugin::init(std::vector) { weechat_printf(nullptr, "%s: It works!", this->name().data()); - return weechat_plugin_init(); + return c::weechat_plugin_init(); } bool plugin::end() { - weechat_plugin_end(); + c::weechat_plugin_end(); return true; } diff --git a/plugin.h b/plugin.h new file mode 100644 index 0000000..ac2e6ca --- /dev/null +++ b/plugin.h @@ -0,0 +1,18 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_PLUGIN_H_ +#define _WEECHAT_XMPP_PLUGIN_H_ + +#ifndef __cplusplus +#define weechat_plugin weechat_xmpp_plugin() +#define WEECHAT_XMPP_PLUGIN_NAME weechat_xmpp_plugin_name() +#define WEECHAT_XMPP_PLUGIN_VERSION weechat_xmpp_plugin_version() +#endif//__cplusplus + +struct t_weechat_plugin *weechat_xmpp_plugin(); +const char *weechat_xmpp_plugin_name(); +const char *weechat_xmpp_plugin_version(); + +#endif /*WEECHAT_XMPP_PLUGIN_H*/ diff --git a/user.c b/user.c new file mode 100644 index 0000000..5190f69 --- /dev/null +++ b/user.c @@ -0,0 +1,255 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include + +#include "plugin.h" +#include "account.h" +#include "user.h" +#include "channel.h" + +const char *user__get_colour(struct t_user *user) +{ + return weechat_info_get("nick_color", user->profile.display_name); +} + +const char *user__get_colour_for_nicklist(struct t_user *user) +{ + return weechat_info_get("nick_color_name", user->profile.display_name); +} + +const char *user__as_prefix_raw(struct t_account *account, + const char *name) +{ + static char result[2048]; + + (void) account; + + snprintf(result, sizeof(result), "%s%s%s", + weechat_info_get("nick_color", name), + name, weechat_color("reset")); + + return result; +} + +const char *user__as_prefix(struct t_account *account, + struct t_user *user, + const char *name) +{ + static char result[2048]; + + (void) account; + + snprintf(result, sizeof(result), "%s%s\t", + user__get_colour(user), + name ? name : user->profile.display_name); + + return result; +} + +struct t_user *user__bot_search(struct t_account *account, + const char *pgp_id) +{ + struct t_user *ptr_user; + + if (!account || !pgp_id) + return NULL; + + for (ptr_user = account->users; ptr_user; + ptr_user = ptr_user->next_user) + { + if (ptr_user->profile.pgp_id && + weechat_strcasecmp(ptr_user->profile.pgp_id, pgp_id) == 0) + return ptr_user; + } + + return NULL; +} + +struct t_user *user__search(struct t_account *account, + const char *id) +{ + struct t_user *ptr_user; + + if (!account || !id) + return NULL; + + for (ptr_user = account->users; ptr_user; + ptr_user = ptr_user->next_user) + { + if (weechat_strcasecmp(ptr_user->id, id) == 0) + return ptr_user; + } + + return NULL; +} + +void user__nicklist_add(struct t_account *account, + struct t_channel *channel, + struct t_user *user) +{ + struct t_gui_nick_group *ptr_group; + struct t_gui_buffer *ptr_buffer; + char *name = channel ? user->profile.display_name : user->id; + if (channel && weechat_strcasecmp(xmpp_jid_bare(account->context, name), + channel->id) == 0) + name = xmpp_jid_resource(account->context, name); + + ptr_buffer = channel ? channel->buffer : account->buffer; + + char *group = "..."; + if (weechat_strcasecmp(user->profile.affiliation, "outcast") == 0) + group = "!"; + if (weechat_strcasecmp(user->profile.role, "visitor") == 0) + group = "?"; + if (weechat_strcasecmp(user->profile.role, "participant") == 0) + group = "+"; + if (weechat_strcasecmp(user->profile.affiliation, "member") == 0) + group = "%"; + if (weechat_strcasecmp(user->profile.role, "moderator") == 0) + group = "@"; + if (weechat_strcasecmp(user->profile.affiliation, "admin") == 0) + group = "&"; + if (weechat_strcasecmp(user->profile.affiliation, "owner") == 0) + group = "~"; + ptr_group = weechat_nicklist_search_group(ptr_buffer, NULL, group); + weechat_nicklist_add_nick(ptr_buffer, ptr_group, + name, + user->is_away ? + "weechat.color.nicklist_away" : + user__get_colour_for_nicklist(user), + group, + "bar_fg", + 1); +} + +void user__nicklist_remove(struct t_account *account, + struct t_channel *channel, + struct t_user *user) +{ + struct t_gui_nick *ptr_nick; + struct t_gui_buffer *ptr_buffer; + char *name = user->profile.display_name; + if (channel && weechat_strcasecmp(xmpp_jid_bare(account->context, name), + channel->id) == 0) + name = xmpp_jid_resource(account->context, name); + + ptr_buffer = channel ? channel->buffer : account->buffer; + + if ((ptr_nick = weechat_nicklist_search_nick(ptr_buffer, NULL, name))) + weechat_nicklist_remove_nick(ptr_buffer, ptr_nick); +} + +struct t_user *user__new(struct t_account *account, + const char *id, const char *display_name) +{ + struct t_user *new_user, *ptr_user; + + if (!account || !id) + { + return NULL; + } + + if (!account->users) + channel__add_nicklist_groups(account, NULL); + + ptr_user = user__search(account, id); + if (ptr_user) + { + user__nicklist_add(account, NULL, ptr_user); + return ptr_user; + } + + if ((new_user = malloc(sizeof(*new_user))) == NULL) + { + return NULL; + } + + new_user->prev_user = account->last_user; + new_user->next_user = NULL; + if (account->last_user) + (account->last_user)->next_user = new_user; + else + account->users = new_user; + account->last_user = new_user; + + new_user->id = strdup(id); + new_user->name = NULL; + + new_user->profile.avatar_hash = NULL; + new_user->profile.status_text = NULL; + new_user->profile.status = NULL; + new_user->profile.idle = NULL; + new_user->profile.display_name = display_name ? + strdup(display_name) : strdup(""); + new_user->profile.affiliation = NULL; + new_user->profile.email = NULL; + new_user->profile.role = NULL; + new_user->profile.pgp_id = NULL; + new_user->updated = 0; + new_user->is_away = 0; + + user__nicklist_add(account, NULL, new_user); + + return new_user; +} + +void user__free(struct t_account *account, + struct t_user *user) +{ + struct t_user *new_users; + + if (!account || !user) + return; + + /* remove user from users list */ + if (account->last_user == user) + account->last_user = user->prev_user; + if (user->prev_user) + { + (user->prev_user)->next_user = user->next_user; + new_users = account->users; + } + else + new_users = user->next_user; + + if (user->next_user) + (user->next_user)->prev_user = user->prev_user; + + /* free user data */ + if (user->id) + free(user->id); + if (user->name) + free(user->name); + if (user->profile.avatar_hash) + free(user->profile.avatar_hash); + if (user->profile.status_text) + free(user->profile.status_text); + if (user->profile.status) + free(user->profile.status); + if (user->profile.idle) + free(user->profile.idle); + if (user->profile.display_name) + free(user->profile.display_name); + if (user->profile.affiliation) + free(user->profile.affiliation); + if (user->profile.email) + free(user->profile.email); + if (user->profile.role) + free(user->profile.role); + + free(user); + + account->users = new_users; +} + +void user__free_all(struct t_account *account) +{ + while (account->users) + user__free(account, account->users); +} diff --git a/user.h b/user.h new file mode 100644 index 0000000..9c153c5 --- /dev/null +++ b/user.h @@ -0,0 +1,60 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_USER_H_ +#define _WEECHAT_XMPP_USER_H_ + +struct t_user_profile +{ + char *avatar_hash; + char *status_text; + char *status; + char *idle; + char *display_name; + char *email; + char *role; + char *affiliation; + char *pgp_id; +}; + +struct t_user +{ + char *id; + char *name; + + struct t_user_profile profile; + int updated; + int is_away; + + struct t_user *prev_user; + struct t_user *next_user; +}; +struct t_channel; + +const char *user__get_colour(struct t_user *user); + +const char *user__as_prefix_raw(struct t_account *account, + const char *name); + +const char *user__as_prefix(struct t_account *account, + struct t_user *user, + const char *name); + +struct t_user *user__search(struct t_account *account, + const char *id); + +struct t_user *user__new(struct t_account *account, + const char *id, const char *display_name); + +void user__free_all(struct t_account *account); + +void user__nicklist_add(struct t_account *account, + struct t_channel *channel, + struct t_user *user); + +void user__nicklist_remove(struct t_account *account, + struct t_channel *channel, + struct t_user *user); + +#endif /*WEECHAT_XMPP_USER_H*/ diff --git a/util.c b/util.c new file mode 100644 index 0000000..382ee05 --- /dev/null +++ b/util.c @@ -0,0 +1,49 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include + +#include "plugin.h" +#include "util.h" + +int char_cmp(const void *p1, const void *p2) +{ + return *(const char *)p1 == *(const char *)p2; +} + +char *exec(const char *command) +{ + // use hook_process instead! + char buffer[128]; + char **result = weechat_string_dyn_alloc(256); + + // Open pipe to file + FILE* pipe = popen(command, "r"); + if (!pipe) { + return "popen failed!"; + } + + // read till end of process: + while (!feof(pipe)) { + + // use buffer to read and add to result + if (fgets(buffer, 128, pipe) != NULL) + weechat_string_dyn_concat(result, buffer, -1); + } + + pclose(pipe); + weechat_string_dyn_free(result, 0); + return *result; +} + +char *stanza_xml(xmpp_stanza_t *stanza) +{ + char *result; + size_t len; + xmpp_stanza_to_text(stanza, &result, &len); + return result; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..0c23aa9 --- /dev/null +++ b/util.h @@ -0,0 +1,14 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_UTIL_H_ +#define _WEECHAT_XMPP_UTIL_H_ + +int char_cmp(const void *p1, const void *p2); + +char *exec(const char *command); + +char *stanza_xml(struct _xmpp_stanza_t *stanza); + +#endif /*WEECHAT_XMPP_UTIL_H*/ diff --git a/xmpp/iq.c b/xmpp/iq.c new file mode 100644 index 0000000..91c70e5 --- /dev/null +++ b/xmpp/iq.c @@ -0,0 +1,403 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include + +#include "stanza.h" + +xmpp_stanza_t *stanza__iq(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, char *ns, char *id, + char *from, char *to, char *type) +{ + xmpp_stanza_t *parent = base ? base : xmpp_iq_new(context, type, id); + xmpp_stanza_t **child = children; + + if (ns) + { + xmpp_stanza_set_ns(parent, ns); + free(ns); + } + + if (base && id) + { + xmpp_stanza_set_id(parent, id); + free(id); + } + + if (from) + { + xmpp_stanza_set_from(parent, from); + free(from); + } + + if (to) + { + xmpp_stanza_set_to(parent, to); + free(to); + } + + if (base && type) + { + xmpp_stanza_set_attribute(parent, "type", type); + free(type); + } + + while (*child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child); + + ++child; + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, struct t_string *ns) +{ + xmpp_stanza_t *parent = base; + xmpp_stanza_t **child = children; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "pubsub"); + } + + if (ns) + { + xmpp_stanza_set_ns(parent, ns->value); + ns->finalize(ns); + free(ns); + } + + while (*child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child); + + ++child; + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_items(xmpp_ctx_t *context, xmpp_stanza_t *base, char *node) +{ + xmpp_stanza_t *parent = base; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "items"); + } + + if (node) + { + xmpp_stanza_set_attribute(parent, "node", node); + free(node); + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_publish(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, struct t_string *node) +{ + xmpp_stanza_t *parent = base; + xmpp_stanza_t **child = children; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "publish"); + } + + if (node) + { + xmpp_stanza_set_attribute(parent, "node", node->value); + node->finalize(node); + free(node); + } + + while (*child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child); + + ++child; + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_publish_item(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, struct t_string *id) +{ + xmpp_stanza_t *parent = base; + xmpp_stanza_t **child = children; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "item"); + } + + if (id) + { + xmpp_stanza_set_id(parent, id->value); + id->finalize(id); + free(id); + } + + while (*child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child); + + ++child; + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_list(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, struct t_string *ns) +{ + xmpp_stanza_t *parent = base; + xmpp_stanza_t **child = children; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "list"); + } + + if (ns) + { + xmpp_stanza_set_ns(parent, ns->value); + ns->finalize(ns); + free(ns); + } + + while (*child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child++); + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_list_device(xmpp_ctx_t *context, xmpp_stanza_t *base, + struct t_string *id) +{ + xmpp_stanza_t *parent = base; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "device"); + } + + if (id) + { + xmpp_stanza_set_id(parent, id->value); + id->finalize(id); + free(id); + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, struct t_string *ns) +{ + xmpp_stanza_t *parent = base; + xmpp_stanza_t **child = children; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "bundle"); + } + + if (ns) + { + xmpp_stanza_set_ns(parent, ns->value); + ns->finalize(ns); + free(ns); + } + + while (child && *child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child++); + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle_signedPreKeyPublic( + xmpp_ctx_t *context, xmpp_stanza_t *base, xmpp_stanza_t **children, struct t_string *signedPreKeyId) +{ + xmpp_stanza_t *parent = base; + xmpp_stanza_t **child = children; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "signedPreKeyPublic"); + } + + if (signedPreKeyId) + { + xmpp_stanza_set_attribute(parent, "signedPreKeyId", signedPreKeyId->value); + signedPreKeyId->finalize(signedPreKeyId); + free(signedPreKeyId); + } + + while (child && *child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child++); + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle_signedPreKeySignature( + xmpp_ctx_t *context, xmpp_stanza_t *base, xmpp_stanza_t **children) +{ + xmpp_stanza_t *parent = base; + xmpp_stanza_t **child = children; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "signedPreKeySignature"); + } + + while (child && *child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child++); + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle_identityKey( + xmpp_ctx_t *context, xmpp_stanza_t *base, xmpp_stanza_t **children) +{ + xmpp_stanza_t *parent = base; + xmpp_stanza_t **child = children; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "identityKey"); + } + + while (child && *child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child++); + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle_prekeys( + xmpp_ctx_t *context, xmpp_stanza_t *base, xmpp_stanza_t **children) +{ + xmpp_stanza_t *parent = base; + xmpp_stanza_t **child = children; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "prekeys"); + } + + while (child && *child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child++); + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle_prekeys_preKeyPublic( + xmpp_ctx_t *context, xmpp_stanza_t *base, xmpp_stanza_t **children, struct t_string *preKeyId) +{ + xmpp_stanza_t *parent = base; + xmpp_stanza_t **child = children; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "preKeyPublic"); + } + + if (preKeyId) + { + xmpp_stanza_set_attribute(parent, "preKeyId", preKeyId->value); + preKeyId->finalize(preKeyId); + free(preKeyId); + } + + while (child && *child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child++); + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_enable(xmpp_ctx_t *context, xmpp_stanza_t *base, + struct t_string *ns) +{ + xmpp_stanza_t *parent = base; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "enable"); + } + + if (ns) + { + xmpp_stanza_set_ns(parent, ns->value); + ns->finalize(ns); + free(ns); + } + + return parent; +} + +xmpp_stanza_t *stanza__iq_ping(xmpp_ctx_t *context, xmpp_stanza_t *base, + struct t_string *ns) +{ + xmpp_stanza_t *parent = base; + + if (!parent) + { + parent = xmpp_stanza_new(context); + xmpp_stanza_set_name(parent, "ping"); + } + + if (ns) + { + xmpp_stanza_set_ns(parent, ns->value); + ns->finalize(ns); + free(ns); + } + + return parent; +} diff --git a/xmpp/presence.c b/xmpp/presence.c new file mode 100644 index 0000000..94a367d --- /dev/null +++ b/xmpp/presence.c @@ -0,0 +1,48 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include + +xmpp_stanza_t *stanza__presence(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, char *ns, + char *from, char *to, char *type) +{ + xmpp_stanza_t *parent = base ? base : xmpp_presence_new(context); + xmpp_stanza_t **child = children; + + if (ns) + { + xmpp_stanza_set_ns(parent, ns); + free(ns); + } + + if (from) + { + xmpp_stanza_set_from(parent, from); + free(from); + } + + if (to) + { + xmpp_stanza_set_to(parent, to); + free(to); + } + + if (type) + { + xmpp_stanza_set_attribute(parent, "type", type); + free(type); + } + + while (*child) + { + xmpp_stanza_add_child(parent, *child); + xmpp_stanza_release(*child); + + ++child; + } + + return parent; +} diff --git a/xmpp/stanza.h b/xmpp/stanza.h new file mode 100644 index 0000000..e63f7de --- /dev/null +++ b/xmpp/stanza.h @@ -0,0 +1,101 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _WEECHAT_XMPP_STANZA_H_ +#define _WEECHAT_XMPP_STANZA_H_ + +struct t_string +{ + char *value; + + void (*finalize)(struct t_string *); + void *pointer; +}; + +static void t_string_noop(struct t_string *string) +{ (void)string; } + +static void t_string_free(struct t_string *string) +{ free(string->value); } + +static void t_string_xmpp_free(struct t_string *string) +{ xmpp_free(string->pointer, string->value); } + +static inline struct t_string *with_noop(const char *const value) +{ + struct t_string *string = malloc(sizeof(struct t_string)); + string->value = (char*)value; + string->finalize = &t_string_noop; + string->pointer = NULL; + return string; +} + +static inline struct t_string *with_free(char *value) +{ + struct t_string *string = malloc(sizeof(struct t_string)); + string->value = value; + string->finalize = &t_string_free; + string->pointer = NULL; + return string; +} + +static inline struct t_string *with_xmpp_free(char *value, xmpp_ctx_t *pointer) +{ + struct t_string *string = malloc(sizeof(struct t_string)); + string->value = value; + string->finalize = &t_string_xmpp_free; + string->pointer = pointer; + return string; +} + +xmpp_stanza_t *stanza__presence(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, const char *ns, + char *from, char *to, const char *type); + +xmpp_stanza_t *stanza__iq(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, char *ns, char *id, + char *from, char *to, char *type); + +xmpp_stanza_t *stanza__iq_pubsub(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, struct t_string *ns); + +xmpp_stanza_t *stanza__iq_pubsub_items(xmpp_ctx_t *context, xmpp_stanza_t *base, char *node); + +xmpp_stanza_t *stanza__iq_pubsub_publish(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, struct t_string *node); + +xmpp_stanza_t *stanza__iq_pubsub_publish_item(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, struct t_string *id); + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_list(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, struct t_string *ns); + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_list_device(xmpp_ctx_t *context, xmpp_stanza_t *base, + struct t_string *id); + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle(xmpp_ctx_t *context, xmpp_stanza_t *base, + xmpp_stanza_t **children, struct t_string *ns); + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle_signedPreKeyPublic( + xmpp_ctx_t *context, xmpp_stanza_t *base, xmpp_stanza_t **children, struct t_string *signedPreKeyId); + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle_signedPreKeySignature( + xmpp_ctx_t *context, xmpp_stanza_t *base, xmpp_stanza_t **children); + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle_identityKey( + xmpp_ctx_t *context, xmpp_stanza_t *base, xmpp_stanza_t **children); + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle_prekeys( + xmpp_ctx_t *context, xmpp_stanza_t *base, xmpp_stanza_t **children); + +xmpp_stanza_t *stanza__iq_pubsub_publish_item_bundle_prekeys_preKeyPublic( + xmpp_ctx_t *context, xmpp_stanza_t *base, xmpp_stanza_t **children, struct t_string *preKeyId); + +xmpp_stanza_t *stanza__iq_enable(xmpp_ctx_t *context, xmpp_stanza_t *base, + struct t_string *ns); + +xmpp_stanza_t *stanza__iq_ping(xmpp_ctx_t *context, xmpp_stanza_t *base, + struct t_string *ns); + +#endif /*WEECHAT_XMPP_STANZA_H*/