├── lib └── .gitignore ├── obj └── .gitignore ├── .gitignore ├── src ├── libtwirc_internal.h ├── libtwirc_util.c ├── tcpsock.h ├── libtwirc.h ├── libtwirc_cmds.c ├── libtwirc_evts.c └── libtwirc.c ├── todo.md ├── README.md └── LICENSE /lib/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /obj/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | token 3 | notes 4 | install 5 | debug-build 6 | -------------------------------------------------------------------------------- /src/libtwirc_internal.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBTWIRC_INTERNAL_H 2 | #define LIBTWIRC_INTERNAL_H 3 | 4 | #include "libtwirc.h" 5 | 6 | /* 7 | * Structures 8 | */ 9 | 10 | struct twirc_state 11 | { 12 | int status : 8; // Connection/login status 13 | int ip_type; // IP type, IPv4 or IPv6 14 | int socket_fd; // TCP socket file descriptor 15 | char *buffer; // IRC message buffer 16 | twirc_login_t login; // IRC login data 17 | twirc_callbacks_t cbs; // Event callbacks 18 | int epfd; // epoll file descriptor 19 | int error; // Last error that occured 20 | void *context; // Pointer to user data 21 | }; 22 | 23 | /* 24 | * Private functions 25 | */ 26 | 27 | static int libtwirc_send(twirc_state_t *s, const char *msg); 28 | static int libtwirc_recv(twirc_state_t *s, char *buf, size_t len); 29 | static int libtwirc_auth(twirc_state_t *s); 30 | static int libtwirc_capreq(twirc_state_t *s); 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Login data 4 | 5 | - Will the fields be NULL if not set? 6 | - Relevant for user/pass in case of anon connection 7 | - Relevant for name/id if not (yet) receieved 8 | 9 | ## Transient nature of data 10 | 11 | - Event info reveived in callbacks will be free'd once callback returns 12 | - This means user has to duplicate/copy data to store/use them after 13 | 14 | ## Change in interface 15 | 16 | - `twirc_login` became `twirc_get_login` 17 | - `twirc_last_error` became `twirc_get_last_error` 18 | - `twirc_tag_by_key` became `twirc_get_tag_by_key` 19 | - Also, it now returns the `*twirc_tag_t` instead of just the value! 20 | 21 | ## Anonymous connection 22 | 23 | - You don't _need_ login data; you can connect anonymously 24 | - This needs to be clarified in the section _Simple Example_ 25 | - This should be explained in a new section, _Connecting_ 26 | 27 | 28 | # Features and bugfixes (required) 29 | 30 | - Consistently set the error code, for example for 'Out of memory' 31 | 32 | 33 | # Features and bugfixes (optional) 34 | 35 | - Implement Room support 36 | - Implement SSL/HTTS/whatever support 37 | 38 | -------------------------------------------------------------------------------- /src/libtwirc_util.c: -------------------------------------------------------------------------------- 1 | #include // NULL, fprintf(), perror() 2 | #include "libtwirc.h" 3 | 4 | /* 5 | * Returns 1 if state is currently connecting to Twitch IRC, otherwise 0. 6 | */ 7 | int 8 | twirc_is_connecting(const twirc_state_t *state) 9 | { 10 | return state->status & TWIRC_STATUS_CONNECTING ? 1 : 0; 11 | } 12 | 13 | /* 14 | * Returns 1 if state is connected to Twitch IRC, otherwise 0. 15 | */ 16 | int 17 | twirc_is_connected(const twirc_state_t *state) 18 | { 19 | return state->status & TWIRC_STATUS_CONNECTED ? 1 : 0; 20 | } 21 | 22 | /* 23 | * Returns 1 if state is currently authenticating, otherwise 0. 24 | */ 25 | int 26 | twirc_is_logging_in(const twirc_state_t *state) 27 | { 28 | return state->status & TWIRC_STATUS_AUTHENTICATING ? 1 : 0; 29 | } 30 | 31 | /* 32 | * Returns 1 if state is authenticated (logged in), otherwise 0. 33 | */ 34 | int 35 | twirc_is_logged_in(const twirc_state_t *state) 36 | { 37 | return state->status & TWIRC_STATUS_AUTHENTICATED ? 1 : 0; 38 | } 39 | 40 | /* 41 | * Return the login struct, which contains login and user data. 42 | */ 43 | twirc_login_t* 44 | twirc_get_login(twirc_state_t *state) 45 | { 46 | return &state->login; 47 | } 48 | 49 | /* 50 | * Searches the provided array of twirc_tag structs for a tag with the 51 | * provided key, then returns a pointer to that tag. If no tag with the 52 | * given key was found, NULL will be returned. 53 | */ 54 | twirc_tag_t* 55 | twirc_get_tag(twirc_tag_t **tags, const char *key) 56 | { 57 | for (int i = 0; tags[i] != NULL; ++i) 58 | { 59 | if (strcmp(tags[i]->key, key) == 0) 60 | { 61 | return tags[i]; 62 | } 63 | } 64 | return NULL; 65 | } 66 | 67 | /* 68 | * Deprecated alias of twirc_get_tag(), use that instead. 69 | */ 70 | twirc_tag_t* 71 | twirc_get_tag_by_key(twirc_tag_t **tags, const char *key) 72 | { 73 | return twirc_get_tag(tags, key); 74 | } 75 | 76 | /* 77 | * Searches the provided array of twirc_tag structs for a tag with the 78 | * provided key, then returns a pointer to that tag's value. If no tag 79 | * with the given key was found, NULL will be returned. 80 | */ 81 | char const* 82 | twirc_get_tag_value(twirc_tag_t **tags, const char *key) 83 | { 84 | for (int i = 0; tags[i] != NULL; ++i) 85 | { 86 | if (strcmp(tags[i]->key, key) == 0) 87 | { 88 | return tags[i]->value; 89 | } 90 | } 91 | return NULL; 92 | } 93 | 94 | /* 95 | * Return the error code of the last error or -1 if non occurred so far. 96 | */ 97 | int 98 | twirc_get_last_error(const twirc_state_t *state) 99 | { 100 | return state->error; 101 | } 102 | 103 | void 104 | twirc_set_context(twirc_state_t *s, void *ctx) 105 | { 106 | s->context = ctx; 107 | } 108 | 109 | void 110 | *twirc_get_context(twirc_state_t *s) 111 | { 112 | return s->context; 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libtwirc 2 | 3 | `libtwirc` is a Twitch IRC client library written in C, developed on and for Linux. It allows you to easily implement chat bots or clients for Twitch with C or any language that can call into C libraries. The interface is pretty similar to that of `libircclient`. 4 | 5 | `libtwirc` specifically implements the Twitch IRC flavor. This means that many features described in the IRC protocol are not supported, most notably `DCC` and SSL. On the other hand, IRCv3 tags, `CAP REQ`, `WHISPER` and other Twitch-specific commands are supported. 6 | 7 | Part of the development happens live on Twitch: [twitch.tv/domsson](https://twitch.tv/domsson) 8 | 9 | # Status 10 | 11 | The library is feature-complete for the current (initial) release and seems to work well. However, I have not unit-tested it yet, so there are most likely some bugs lurking still. I would love some feedback from actual users, so why not give it a try? 12 | 13 | # How to use 14 | 15 | Check out the [wiki](https://github.com/domsson/libtwirc/wiki), which should have all the information to get you started. Also, there is some example code available over at [twircclient](https://github.com/domsson/twircclient). The rough and overly simplified outline of using `libtwirc` looks something like this: 16 | 17 | ``` 18 | twirc_state_t *s = twirc_init(); // create twirc state 19 | twirc_connect(s, "irc.chat.twitch.tv", "6667", USER, TOKEN); // connect to Twitch IRC 20 | twirc_loop(s); // run in a loop until disconnected 21 | twirc_kill(s); // free resources 22 | ``` 23 | 24 | # Motivation 25 | 26 | I wanted to write a Twitch chat bot in C. I found `libircclient` and was using it happily, but ran into two issues. First, it doesn't support IRCv3 tags, which Twitch is using. Second, it uses a GPL license. Now, my bot (and almost all my software) is CC0 (aka public domain) and even after more than 4 hours of research, I couldn't figure out if I would be able to release my code as CC0 when using a GPL licensend library. This, plus the fact that I'm still learning C and am looking for small projects to help me gain more experience, I decided to write my own IRC library. To keep the scope smaller, I decided to make it Twitch-specific and, for now, Linux only. 27 | 28 | # Code style 29 | 30 | If you want to contribute code or are, for whatever reason, otherwise interested in my "code guidelines", here is a rough list: 31 | 32 | - I keep it simple, 'cause I'm stupid 33 | - I add plenty of comments for future me 34 | - I always use braces, even for one-liners 35 | - I always put braces on their own lines 36 | - I prefix public functions with `twirc_` 37 | - I prefix private functions with `libtwirc_` 38 | - I try to use little memory (lots of `malloc`) 39 | - I put the asterisk next to the var/func name 40 | - I prefer many small funcs over a few big ones 41 | - I only use `typedef` sparingly 42 | - I add `_t` to typedef'd variable names 43 | - I try to keep lines to 80 chars if possible 44 | - I rather exceed 80 chars a bit than wrap a line 45 | - I rather wrap a line than exceed 80 chars by a lot 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /src/tcpsock.h: -------------------------------------------------------------------------------- 1 | #ifndef TCPSOCK_H 2 | #define TCPSOCK_H 3 | 4 | #include // NULL, EXIT_FAILURE, EXIT_SUCCESS 5 | #include // close(), fcntl() 6 | #include // errno 7 | #include // fcntl() 8 | #include // ssize_t 9 | #include // socket(), connect(), send(), recv() 10 | #include // getaddrinfo() 11 | 12 | // 13 | // API 14 | // 15 | 16 | #define TCPSOCK_IPV4 AF_INET 17 | #define TCPSOCK_IPV6 AF_INET6 18 | 19 | #define TCPSOCK_NONBLOCK 0 20 | #define TCPSOCK_BLOCK 1 21 | 22 | /* 23 | * Creates a non-blocking TCP socket, either IPv4 or IPv6, depending on ip_type. 24 | * If the given ip_type is neither AF_INET nor AF_INET6, AF_INET (IPv4) is used. 25 | * Returns the socket's file descriptor on success or -1 on error. Check errno. 26 | * In case of error, either we failed to create a socket via socket(), or a call 27 | * to fcntl(), in an attempt to get or set the file descriptor flags, failed. 28 | */ 29 | int tcpsock_create(int ip_type, int block); 30 | 31 | /* 32 | * Checks if the given socket is blocking. Returns 0 for non-blocking sockets, 33 | * 1 for blocking sockets. If aquiring the socket's blocking status failed, -1 34 | * is returned and errno will be set accordingly. 35 | */ 36 | int tcpsock_blocking(int sockfd); 37 | 38 | /* 39 | * Initiates a connection for the TCP socket described by sockfd. 40 | * The ip_type should match the one used when the socket was created. 41 | * If the given ip_type is neither AF_INET nor AF_INET6, AF_INET (IPv4) is used. 42 | * Returns 0 if the connection was successfully initiated (is now in progress). 43 | * Returns -1 if the host/port could not be translated into an IP address or if 44 | * the connection could not be established for some other reason. 45 | */ 46 | int tcpsock_connect(int sockfd, int ip_type, const char *host, const char *port); 47 | 48 | /* 49 | * Queries getsockopt() for the socket status in an attempt to figure out 50 | * whether the socket is connected. Note that this should not be used unless 51 | * there is a good reason - it is always best to simply try and send on the 52 | * socket in question to see if it is connected. If you want to check if a 53 | * previous connection attempt succeeded, you should simply use select(), 54 | * poll() or epoll() to wait on the socket and see if it becomes ready for 55 | * writing (sending); this indicates the socket connection is established. 56 | * Returns 0 if the socket is healthy and most likely connected. 57 | * Returns -1 if the socket reported an error or the socket status could not 58 | * be queried, both indicating that the socket is most likely not connected. 59 | */ 60 | int tcpsock_status(int sockfd); 61 | 62 | /* 63 | * Sends the given data using the given socket. 64 | * On success, this function returns the number of bytes sent. 65 | * On error, -1 is returned and errno is set appropriately. 66 | * See the man page of send() for more details. 67 | */ 68 | int tcpsock_send(int sockfd, const char *msg, size_t len); 69 | 70 | /* 71 | * Reads the buffer of the given socket using recv(). 72 | * On success, this function returns the number of bytes received. 73 | * On error, -1 is returend and errno is set appropriately. 74 | * Returns 0 if the if the socket connection was shut down by peer 75 | * or if the requested number of bytes to receive from the socket was 0. 76 | * See the man page of recv() for more details. 77 | */ 78 | int tcpsock_receive(int sockfd, char *buf, size_t len); 79 | 80 | /* 81 | * Closes the given socket. 82 | * Returns 0 on success, -1 on error (see errno). 83 | * See the man page of close() for more details. 84 | */ 85 | int tcpsock_close(int sockfd); 86 | 87 | // 88 | // IMPLEMENTATION 89 | // 90 | 91 | #ifdef TCPSOCK_IMPLEMENTATION 92 | 93 | int tcpsock_create(int ip_type, int block) 94 | { 95 | // If ip_type was neither IPv4 nor IPv6, we fall back to IPv4 96 | if ((ip_type != AF_INET) && (ip_type != AF_INET6)) 97 | { 98 | ip_type = AF_INET; 99 | } 100 | 101 | // This line could replace all of the follwing but isn't POSIX! 102 | // int sfd = socket(s->ip_type, SOCK_STREAM | SOCK_NONBLOCK, 0); 103 | 104 | // Open a TCP socket (SOCK_STREAM) 105 | int sfd = socket(ip_type, SOCK_STREAM, 0); 106 | if (sfd == -1) 107 | { 108 | return -1; 109 | } 110 | 111 | // Nonblocking, we're done 112 | if (block == 0) 113 | { 114 | // All done, return socket file descriptor 115 | return sfd; 116 | } 117 | 118 | // Get the current file descriptor flags 119 | int get = fcntl(sfd, F_GETFL); 120 | if (get == -1) 121 | { 122 | return -1; 123 | } 124 | 125 | // Add O_NONBLOCK to the file descriptor flags 126 | int set = fcntl(sfd, F_SETFL, get | O_NONBLOCK); 127 | if (set == -1) 128 | { 129 | return -1; 130 | } 131 | 132 | // All done, return socket file descriptor 133 | return sfd; 134 | } 135 | 136 | int tcpsock_blocking(int sockfd) 137 | { 138 | int flags = fcntl(sockfd, F_GETFL); 139 | if (flags == -1) 140 | { 141 | return -1; 142 | } 143 | 144 | return (flags & O_NONBLOCK) != O_NONBLOCK; 145 | } 146 | 147 | int tcpsock_connect(int sockfd, int ip_type, const char *host, const char *port) 148 | { 149 | // Figure out if the socket is blocking 150 | int block = tcpsock_blocking(sockfd); 151 | if (block == -1) 152 | { 153 | // Couldn't figure out if socket is blocking or non-blocking 154 | return -1; 155 | } 156 | 157 | // If ip_type was neither IPv4 nor IPv6, we fall back to IPv4 158 | if ((ip_type != AF_INET) && (ip_type != AF_INET6)) 159 | { 160 | ip_type = AF_INET; 161 | } 162 | 163 | // Not initializing the struct with { 0 } will result in garbage values 164 | // that can (but not necessarily will) make getaddrinfo() fail! 165 | struct addrinfo hints = { 0 }; 166 | hints.ai_family = ip_type; 167 | hints.ai_socktype = SOCK_STREAM; 168 | hints.ai_protocol = IPPROTO_TCP; 169 | 170 | struct addrinfo *info = NULL; 171 | if (getaddrinfo(host, port, &hints, &info) != 0) 172 | { 173 | // Calling freeaddrinfo() when getaddrinfo() failed 174 | // will give a segfault in case `info` is still NULL! 175 | if (info != NULL) 176 | { 177 | freeaddrinfo(info); 178 | } 179 | return -1; 180 | } 181 | 182 | // Attempt to initiate a connection 183 | int con = connect(sockfd, info->ai_addr, info->ai_addrlen); 184 | freeaddrinfo(info); 185 | 186 | // connect() should return 0 for success on blocking sockets, -1 for non-blocking sockets 187 | if (con == -1) 188 | { 189 | if (block) 190 | { 191 | // Some error occured (errno will be set) 192 | return -1; 193 | } 194 | 195 | // Connection in progress (that's what we expect!) 196 | if (errno == EINPROGRESS || errno == EALREADY) 197 | { 198 | return 0; 199 | } 200 | // Some other error occured (errno will be set) 201 | return -1; 202 | } 203 | 204 | // connect() returned 0, so we're connected 205 | return 0; 206 | } 207 | 208 | int tcpsock_status(int sockfd) 209 | { 210 | int err = 0; 211 | socklen_t len = sizeof(err); 212 | 213 | if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &len) != 0) 214 | { 215 | // Could not get the socket's status, invalid file descriptor? 216 | return -1; 217 | } 218 | 219 | if (err != 0) 220 | { 221 | // Socket reported some error, so probably not connected 222 | return -1; 223 | } 224 | 225 | // No socket error reported, chances are it is connected 226 | return 0; 227 | } 228 | 229 | int tcpsock_send(int sockfd, const char *msg, size_t len) 230 | { 231 | return send(sockfd, msg, len, 0); 232 | } 233 | 234 | int tcpsock_receive(int sockfd, char *buf, size_t len) 235 | { 236 | return recv(sockfd, buf, len, 0); 237 | } 238 | 239 | int tcpsock_close(int sockfd) 240 | { 241 | return close(sockfd); 242 | } 243 | 244 | #endif /* TCPSOCK_IMPLEMENTATION */ 245 | #endif /* TCPSOCK_H */ 246 | -------------------------------------------------------------------------------- /src/libtwirc.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBTWIRC_H 2 | #define LIBTWIRC_H 3 | 4 | // Name & Version 5 | #define TWIRC_NAME "libtwirc" 6 | #define TWIRC_VER_MAJOR 0 7 | #define TWIRC_VER_MINOR 1 8 | #define TWIRC_VER_BUILD 2 9 | 10 | // Convenience 11 | #define TWIRC_IPV4 TCPSOCK_IPV4 12 | #define TWIRC_IPV6 TCPSOCK_IPV6 13 | 14 | // State (bitfield) 15 | #define TWIRC_STATUS_DISCONNECTED 0 16 | #define TWIRC_STATUS_CONNECTING 1 17 | #define TWIRC_STATUS_CONNECTED 2 18 | #define TWIRC_STATUS_AUTHENTICATING 4 19 | #define TWIRC_STATUS_AUTHENTICATED 8 20 | 21 | // Errors 22 | #define TWIRC_ERR_NONE 0 23 | #define TWIRC_ERR_OUT_OF_MEMORY -2 24 | #define TWIRC_ERR_SOCKET_CREATE -3 25 | #define TWIRC_ERR_SOCKET_CONNECT -4 26 | #define TWIRC_ERR_SOCKET_SEND -5 27 | #define TWIRC_ERR_SOCKET_RECV -6 28 | #define TWIRC_ERR_SOCKET_CLOSE -7 29 | #define TWIRC_ERR_EPOLL_CREATE -8 30 | #define TWIRC_ERR_EPOLL_CTL -9 31 | #define TWIRC_ERR_EPOLL_WAIT -10 // epoll_pwait() error 32 | #define TWIRC_ERR_CONN_CLOSED -11 // Connection lost: peer closed it 33 | #define TWIRC_ERR_CONN_HANGUP -12 // Connection lost: unexpectedly 34 | #define TWIRC_ERR_CONN_SOCKET -13 // Connection lost: socket error 35 | #define TWIRC_ERR_EPOLL_SIG -14 // epoll_pwait() caught a signal 36 | 37 | // Maybe we should do this, too: 38 | // https://github.com/shaoner/libircclient/blob/master/include/libirc_rfcnumeric.h 39 | // Good source is: 40 | // https://www.alien.net.au/irc/irc2numerics.html 41 | 42 | // Message size needs to be large enough to accomodate a single IRC message 43 | // from the Twitch servers. Twitch limits the visible chat message part of 44 | // an IRC message to 512 bytes (510 without \r\n), but does not seem to take 45 | // tags, prefix, command or parameter length into account for the total length 46 | // of the message, which can often result in messages that easily exceed the 47 | // 1024 bytes length limit as described by the IRCv3 spec. According to some 48 | // tests, we should be fine with doubling that to 2048. Note that the internal 49 | // buffer of the twirc_state struct will use a buffer that is twice as big as 50 | // TWIRC_MESSAGE_SIZE in order to be able to accomodate parts of an incomplete 51 | // message in addition to a complete one. 52 | #define TWIRC_MESSAGE_SIZE 2048 53 | 54 | // The buffer size will be used for retrieving network data via recv(), which 55 | // means it could be very small (say, 256 bytes), as we call recv() in a loop 56 | // until all data has been retrieved and processed. However, this will also 57 | // increase the CPU load required; having a larger buffer means we can process 58 | // more data in one go, which will improve speed. It seems sensible to choose 59 | // a size that is at least as large as the MESSAGE buffer (see above) so that 60 | // we can assure that we will be able to process an entire message in one go. 61 | #define TWIRC_BUFFER_SIZE TWIRC_MESSAGE_SIZE 62 | 63 | // The prefix is an optional part of every IRC message retrieved from a server. 64 | // As such, it can never exceed or even reach the size of a message itself. 65 | // Usually, the prefix is a rather short string, based upon the length of the 66 | // user's nickname. 128 would probably do, but 256 seems to be a safer choice. 67 | #define TWIRC_PREFIX_SIZE 256 68 | 69 | // The command is always present, it tells us what kind of message we received. 70 | // this could be PING or PRIVMSG or one of many possible numerical codes. 71 | // The page below lists known commands, the longest seems to be UNLOADMODULE or 72 | // RELOADMODULE, both have a length of 12. Hence, a size of 16 seems enough. 73 | // On the other hand, Twitch has custom commands, the longest of which is, at 74 | // the time of writing, GLOBALUSERSTATE, with a length of 16. Taking the null 75 | // terminator into account, it seems safer to go with a generous 32 bytes here. 76 | #define TWIRC_COMMAND_SIZE 32 77 | 78 | // Total size for the PONG command, including its optional parameter. 79 | // Currently, the command is always "PONG :tmi.twitch.tv", but it might change 80 | // slightly in the future if Twitch is ever changing that URI. Currently, with 81 | // the command taking up 20 bytes, 32 is more than enough of a buffer, but we 82 | // might just go with twice that to be on the safe side. 83 | #define TWIRC_PONG_SIZE 64 84 | 85 | // The length of Twitch user names is limited to 25. Hence, 32 will do us fine. 86 | // https://www.reddit.com/r/Twitch/comments/32w5b2/username_requirements/ 87 | #define TWIRC_NICK_SIZE 32 88 | 89 | // The number of expected tags in an IRC message. This will be used to allocate 90 | // memory for the tags. If this number is smaller than the actual number of 91 | // tags in a message, realloc() will be used to allocate more memory. In other 92 | // words, to improve performance but keep memory footprint small, it would make 93 | // sense to choose this number slightly larger than the average number of tags 94 | // that can be expected in a message. Tests have shown that Twitch IRC usually 95 | // sends 13 tags per IRC message, so 16 seems to be a reasonable choice. 96 | #define TWIRC_NUM_TAGS 16 97 | 98 | // The number of parameters is different depending on the type of the command. 99 | // For most commands, two to three params will be sent, for some 4, for a few 100 | // select commands even more. It seems reasonable to set this to 4, as it will 101 | // cover most cases and only require realloc() in very rare cases, I believe. 102 | // Conveniently, the maximum number of parameters is 15, leaving one for NULL. 103 | // So we'll just realloc twice the size when we run out, going 4 to 8 to 16. 104 | // http://www.networksorcery.com/enp/protocol/irc.htm 105 | #define TWIRC_NUM_PARAMS 4 106 | 107 | // If you want to connect to Twitch IRC anonymously, which means you'll be able 108 | // to read chat but not participate, then you need to use the special username 109 | // "justinfan", which seems to be a relic from the JustinTV days. 110 | #define TWIRC_USER_ANON "justinfan" 111 | 112 | // Defines the maximum number of digits that will be used as a suffix for the 113 | // anonymous username (TWIRC_USER_ANON) 114 | #define TWIRC_USER_ANON_MAX_DIGITS 7 115 | 116 | /* 117 | * Structures 118 | */ 119 | 120 | struct twirc_state; 121 | struct twirc_event; 122 | struct twirc_callbacks; 123 | struct twirc_login; 124 | struct twirc_tag; 125 | 126 | typedef struct twirc_event twirc_event_t; 127 | typedef struct twirc_login twirc_login_t; 128 | typedef struct twirc_tag twirc_tag_t; 129 | typedef struct twirc_state twirc_state_t; 130 | typedef struct twirc_callbacks twirc_callbacks_t; 131 | 132 | struct twirc_login 133 | { 134 | char *host; 135 | char *port; 136 | char *nick; 137 | char *pass; 138 | char *name; 139 | char *id; 140 | }; 141 | 142 | struct twirc_tag 143 | { 144 | char *key; 145 | char *value; 146 | }; 147 | 148 | struct twirc_event 149 | { 150 | // Raw data 151 | char *raw; // The raw message as received 152 | // Separated raw data 153 | char *prefix; // IRC message prefix 154 | char *command; // IRC message command 155 | char **params; // IRC message parameter 156 | size_t num_params; // Number of elements in params 157 | int trailing; // Index of the trailing param 158 | twirc_tag_t **tags; // IRC message tags 159 | size_t num_tags; // Number of elements in tags 160 | // For convenience 161 | char *origin; // Nick as extracted from prefix 162 | char *channel; // Channel as extracted from params 163 | char *target; // Target user of hosts, bans, etc. 164 | char *message; // Message as extracted from params 165 | char *ctcp; // CTCP commmand, if any 166 | }; 167 | 168 | typedef void (*twirc_callback)(twirc_state_t *s, twirc_event_t *e); 169 | 170 | struct twirc_callbacks 171 | { 172 | twirc_callback connect; // Connection established 173 | twirc_callback welcome; // 001 received (logged in) 174 | twirc_callback globaluserstate; // Logged in (+ user info) 175 | twirc_callback capack; // Capabilities acknowledged 176 | twirc_callback ping; // PING received 177 | twirc_callback join; // User joined a channel 178 | twirc_callback part; // User left a channel 179 | twirc_callback mode; // User gained/lost mod status 180 | twirc_callback names; // Reply to /NAMES command 181 | twirc_callback privmsg; // Regular chat message in a channel 182 | twirc_callback whisper; // Whisper (private message) 183 | twirc_callback action; // CTCP ACTION received 184 | twirc_callback notice; // Notice from server 185 | twirc_callback roomstate; // Channel setting changed OR join 186 | twirc_callback usernotice; // Sub, resub, giftsub, raid, ritual 187 | twirc_callback userstate; // User joins or chats in channel (?) 188 | twirc_callback clearchat; // Chat history purged or user banned 189 | twirc_callback clearmsg; // A chat message has been removed 190 | twirc_callback hosttarget; // Channel starts or stops host mode 191 | twirc_callback reconnect; // Server is going for a restart soon 192 | twirc_callback disconnect; // Connection interrupted 193 | twirc_callback invalidcmd; // Server doesn't recognise command 194 | twirc_callback other; // Everything else (for now) 195 | twirc_callback outbound; // Messages we send TO the server 196 | }; 197 | 198 | /* 199 | * Public functions 200 | */ 201 | 202 | // Initialization 203 | twirc_state_t *twirc_init(); 204 | twirc_callbacks_t *twirc_get_callbacks(twirc_state_t *s); 205 | 206 | // Connecting and disconnecting 207 | int twirc_connect(twirc_state_t *s, const char *host, const char *port, const char *nick, const char *pass); 208 | int twirc_connect_anon(twirc_state_t *s, const char *host, const char *port); 209 | int twirc_disconnect(twirc_state_t *s); 210 | 211 | // Main flow control 212 | int twirc_loop(twirc_state_t *s); 213 | int twirc_tick(twirc_state_t *s, int timeout); 214 | 215 | // Clean-up and shut-down 216 | void twirc_kill(twirc_state_t *s); 217 | void twirc_free(twirc_state_t *s); 218 | 219 | // Retrieval of data from the twirc state 220 | twirc_login_t *twirc_get_login(twirc_state_t *s); 221 | twirc_tag_t *twirc_get_tag_by_key(twirc_tag_t **tags, const char *key); // deprecated 222 | twirc_tag_t *twirc_get_tag(twirc_tag_t **tags, const char *key); 223 | char const *twirc_get_tag_value(twirc_tag_t **tags, const char *key); 224 | int twirc_get_last_error(const twirc_state_t *s); 225 | 226 | // Twitc state status inforamtion 227 | int twirc_is_connecting(const twirc_state_t *s); 228 | int twirc_is_logging_in(const twirc_state_t *s); 229 | int twirc_is_connected(const twirc_state_t *s); 230 | int twirc_is_logged_in(const twirc_state_t *s); 231 | 232 | // Custom user-data 233 | void twirc_set_context(twirc_state_t *s, void *ctx); 234 | void *twirc_get_context(twirc_state_t *s); 235 | 236 | // Twitch IRC commands 237 | int twirc_cmd_raw(twirc_state_t *s, const char *msg); 238 | int twirc_cmd_pass(twirc_state_t *s, const char *pass); 239 | int twirc_cmd_nick(twirc_state_t *s, const char *nick); 240 | int twirc_cmd_join(twirc_state_t *s, const char *chan); 241 | int twirc_cmd_part(twirc_state_t *s, const char *chan); 242 | int twirc_cmd_ping(twirc_state_t *s, const char *param); 243 | int twirc_cmd_pong(twirc_state_t *s, const char *param); 244 | int twirc_cmd_quit(twirc_state_t *s); 245 | int twirc_cmd_privmsg(twirc_state_t *s, const char *chan, const char *msg); 246 | int twirc_cmd_action(twirc_state_t *s, const char *chan, const char *msg); 247 | int twirc_cmd_whisper(twirc_state_t *s, const char *nick, const char *msg); 248 | int twirc_cmd_req_tags(twirc_state_t *s); 249 | int twirc_cmd_req_membership(twirc_state_t *s); 250 | int twirc_cmd_req_commands(twirc_state_t *s); 251 | int twirc_cmd_mods(twirc_state_t *s, const char *chan); 252 | int twirc_cmd_vips(twirc_state_t *s, const char *chan); 253 | int twirc_cmd_color(twirc_state_t *s, const char *color); 254 | int twirc_cmd_delete(twirc_state_t *s, const char *chan, const char *id); 255 | int twirc_cmd_timeout(twirc_state_t *s, const char *chan, const char *nick, int secs, const char *reason); 256 | int twirc_cmd_untimeout(twirc_state_t *s, const char *chan, const char *nick); 257 | int twirc_cmd_ban(twirc_state_t *s, const char *chan, const char *nick, const char *reason); 258 | int twirc_cmd_unban(twirc_state_t *s, const char *chan, const char *nick); 259 | int twirc_cmd_slow(twirc_state_t *s, const char *chan, int secs); 260 | int twirc_cmd_slowoff(twirc_state_t *s, const char *chan); 261 | int twirc_cmd_followers(twirc_state_t *s, const char *chan, const char *time); 262 | int twirc_cmd_followersoff(twirc_state_t *s, const char *chan); 263 | int twirc_cmd_subscribers(twirc_state_t *s, const char *chan); 264 | int twirc_cmd_subscribersoff(twirc_state_t *s, const char *chan); 265 | int twirc_cmd_clear(twirc_state_t *s, const char *chan); 266 | int twirc_cmd_r9k(twirc_state_t *s, const char *chan); 267 | int twirc_cmd_r9koff(twirc_state_t *s, const char *chan); 268 | int twirc_cmd_emoteonly(twirc_state_t *s, const char *chan); 269 | int twirc_cmd_emoteonlyoff(twirc_state_t *s, const char *chan); 270 | int twirc_cmd_commercial(twirc_state_t *s, const char *chan, int secs); 271 | int twirc_cmd_host(twirc_state_t *s, const char *chan, const char *target); 272 | int twirc_cmd_unhost(twirc_state_t *s, const char *chan); 273 | int twirc_cmd_mod(twirc_state_t *s, const char *chan, const char *nick); 274 | int twirc_cmd_unmod(twirc_state_t *s, const char *chan, const char *nick); 275 | int twirc_cmd_vip(twirc_state_t *s, const char *chan, const char *nick); 276 | int twirc_cmd_unvip(twirc_state_t *s, const char *chan, const char *nick); 277 | int twirc_cmd_marker(twirc_state_t *s, const char *chan, const char *comment); 278 | 279 | #endif 280 | -------------------------------------------------------------------------------- /src/libtwirc_cmds.c: -------------------------------------------------------------------------------- 1 | #include // NULL, fprintf(), perror() 2 | #include "libtwirc.h" 3 | #include "libtwirc_internal.h" 4 | 5 | /* 6 | * Convenience function that sends the provided message to the IRC server 7 | * as-is (the only modification being that it will add the required \n\r 8 | * as required by IRC); this is the same as using twirc_send(). 9 | */ 10 | int 11 | twirc_cmd_raw(twirc_state_t *state, const char *msg) 12 | { 13 | return libtwirc_send(state, msg); 14 | } 15 | 16 | /* 17 | * Sends the PASS command to the server, with the pass appended as parameter. 18 | * This is the first part of the authentication process (next part is NICK). 19 | * Returns 0 if the command was sent successfully, -1 on error. 20 | */ 21 | int 22 | twirc_cmd_pass(twirc_state_t *state, const char *pass) 23 | { 24 | char msg[TWIRC_BUFFER_SIZE]; 25 | snprintf(msg, TWIRC_BUFFER_SIZE, "PASS %s", pass); 26 | return libtwirc_send(state, msg); 27 | } 28 | 29 | /* 30 | * Sends the NICK command to the server, with the nick appended as parameter. 31 | * This is the second part of the authentication process (first is PASS). 32 | * Returns 0 if the command was sent successfully, -1 on error. 33 | */ 34 | int 35 | twirc_cmd_nick(twirc_state_t *state, const char *nick) 36 | { 37 | char msg[TWIRC_BUFFER_SIZE]; 38 | snprintf(msg, TWIRC_BUFFER_SIZE, "NICK %s", nick); 39 | return libtwirc_send(state, msg); 40 | } 41 | 42 | /* 43 | * Request to join the specified channel. 44 | * Returns 0 if the command was sent successfully, -1 on error. 45 | */ 46 | int 47 | twirc_cmd_join(twirc_state_t *state, const char *chan) 48 | { 49 | char msg[TWIRC_BUFFER_SIZE]; 50 | snprintf(msg, TWIRC_BUFFER_SIZE, "JOIN %s", chan); 51 | return libtwirc_send(state, msg); 52 | } 53 | 54 | /* 55 | * Leave (part) the specified channel. 56 | * Returns 0 if the command was sent successfully, -1 on error. 57 | */ 58 | int 59 | twirc_cmd_part(twirc_state_t *state, const char *chan) 60 | { 61 | char msg[TWIRC_BUFFER_SIZE]; 62 | snprintf(msg, TWIRC_BUFFER_SIZE, "PART %s", chan); 63 | return libtwirc_send(state, msg); 64 | } 65 | 66 | /* 67 | * Sends the PONG command to the IRC server. 68 | * If param is given, it will be appended. To make Twitch happy (this is not 69 | * part of the IRC specification) the param will be prefixed with a colon (":") 70 | * unless it is prefixed with one already. 71 | * Returns 0 on success, -1 otherwise. 72 | */ 73 | int 74 | twirc_cmd_pong(twirc_state_t *state, const char *param) 75 | { 76 | // "PONG :" 77 | char pong[TWIRC_PONG_SIZE]; 78 | pong[0] = '\0'; 79 | snprintf(pong, TWIRC_PONG_SIZE, "PONG %s%s", 80 | param && param[0] == ':' ? "" : ":", 81 | param ? param : ""); 82 | return libtwirc_send(state, pong); 83 | } 84 | 85 | /* 86 | * Sends the PING command to the IRC server. 87 | * If param is given, it will be appended. 88 | * Returns 0 on success, -1 otherwise. 89 | */ 90 | int 91 | twirc_cmd_ping(twirc_state_t *state, const char *param) 92 | { 93 | // "PING " 94 | char ping[TWIRC_PONG_SIZE]; 95 | ping[0] = '\0'; 96 | snprintf(ping, TWIRC_PONG_SIZE, "PING %s", param ? param : ""); 97 | return libtwirc_send(state, ping); 98 | } 99 | 100 | /* 101 | * Sends the QUIT command to the IRC server. 102 | * Returns 0 on success, -1 otherwise. 103 | */ 104 | int 105 | twirc_cmd_quit(twirc_state_t *state) 106 | { 107 | return libtwirc_send(state, "QUIT"); 108 | } 109 | 110 | /* 111 | * Send a message (privmsg) to the specified channel. 112 | * Returns 0 if the command was sent successfully, -1 on error. 113 | */ 114 | int 115 | twirc_cmd_privmsg(twirc_state_t *state, const char *chan, const char *msg) 116 | { 117 | char privmsg[TWIRC_BUFFER_SIZE]; 118 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :%s", chan, msg); 119 | return libtwirc_send(state, privmsg); 120 | } 121 | 122 | /* 123 | * Send a CTCP ACTION message (aka "/me") to the specified channel. 124 | * Returns 0 if the command was sent successfully, -1 on error. 125 | */ 126 | int 127 | twirc_cmd_action(twirc_state_t *state, const char *chan, const char *msg) 128 | { 129 | // "PRIVMSG # :\x01ACTION \x01" 130 | char action[TWIRC_MESSAGE_SIZE]; 131 | action[0] = '\0'; 132 | snprintf(action, TWIRC_MESSAGE_SIZE, "PRIVMSG %s :%cACTION %s%c", 133 | chan, '\x01', msg, '\x01'); 134 | return libtwirc_send(state, action); 135 | } 136 | 137 | /* 138 | * Send a whisper message to the specified user. 139 | * Returns 0 if the command was sent successfully, -1 on error. 140 | */ 141 | int 142 | twirc_cmd_whisper(twirc_state_t *state, const char *nick, const char *msg) 143 | { 144 | // Usage: "/w " 145 | char whisper[TWIRC_MESSAGE_SIZE]; 146 | snprintf(whisper, TWIRC_MESSAGE_SIZE, "PRIVMSG #%s :/w %s %s", 147 | state->login.nick, nick, msg); 148 | return libtwirc_send(state, whisper); 149 | } 150 | 151 | /* 152 | * Requests a list of the channel's moderators, both offline and online. 153 | * The answer will be in the form of a NOTICE with the msg-id tag set to 154 | * "room_mods" and a message like "The moderators of this channel are: ", 155 | * where list is a comma-and-space separated list of the moderators nicks. 156 | * If the channel has no moderators, the msg-id tag will be "no_mods" instead. 157 | */ 158 | int 159 | twirc_cmd_mods(twirc_state_t *state, const char *chan) 160 | { 161 | char privmsg[TWIRC_BUFFER_SIZE]; 162 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/mods", chan); 163 | return libtwirc_send(state, privmsg); 164 | } 165 | 166 | /* 167 | * Requests a list of the channel's VIPs, both offline and online. 168 | * The answer will be in the form of a NOTICE with the msg-id tag set to either 169 | * "room_vips" or "no_vips" if the room doesn't have any VIPs 170 | */ 171 | int 172 | twirc_cmd_vips(twirc_state_t *state, const char *chan) 173 | { 174 | // Usage: "/vips" - Lists the VIPs of this channel 175 | char privmsg[TWIRC_BUFFER_SIZE]; 176 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/vips", chan); 177 | return libtwirc_send(state, privmsg); 178 | } 179 | 180 | /* 181 | * Change your color to the specified one. If you're a turbo user, this can be 182 | * any hex color (for example, "#FFFFFF" for white), otherwise it should be a 183 | * named color from the following list (might change in the future): 184 | * Blue, BlueViolet, CadetBlue, Chocolate, Coral, DodgerBlue, Firebrick, 185 | * GoldenRod, Green, HotPink, OrangeRed, Red, SeaGreen, SpringGreen, 186 | * YellowGreen 187 | */ 188 | int 189 | twirc_cmd_color(twirc_state_t *state, const char *color) 190 | { 191 | // Usage: "/color " - Change your username color. Color must be 192 | // in hex (#000000) or one of the following: Blue, BlueViolet, CadetBlue, 193 | // Chocolate, Coral, DodgerBlue, Firebrick, GoldenRod, Green, HotPink, 194 | // OrangeRed, Red, SeaGreen, SpringGreen, YellowGreen. 195 | char privmsg[TWIRC_BUFFER_SIZE]; 196 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG #%s :/color %s", 197 | state->login.nick, color); 198 | return libtwirc_send(state, privmsg); 199 | } 200 | 201 | /* 202 | * Broadcasters and Moderators only: 203 | * Delete the message with the specified id in the given channel. 204 | * TODO This has yet to be tested, the usage is still unclear to me. 205 | */ 206 | int 207 | twirc_cmd_delete(twirc_state_t *state, const char *chan, const char *id) 208 | { 209 | // @msg-id=usage_delete :tmi.twitch.tv NOTICE #domsson :%!(EXTRA string=delete) 210 | char privmsg[TWIRC_BUFFER_SIZE]; 211 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/delete %s", 212 | chan, id); 213 | return libtwirc_send(state, privmsg); 214 | } 215 | 216 | /* 217 | * Broadcasters and Moderators only: 218 | * Timeout the user with the given nick name in the specified channel for 219 | * `secs` amount of seconds. If `secs` is 0, the Twitch default will be used, 220 | * which is 600 seconds (10 minutes) at the time of writing. `reason` will be 221 | * shown to the affected user and other moderators and is optional (use NULL). 222 | */ 223 | int 224 | twirc_cmd_timeout(twirc_state_t *state, const char *chan, const char *nick, int secs, const char *reason) 225 | { 226 | // Usage: "/timeout [duration][time unit] [reason]" 227 | // Temporarily prevent a user from chatting. Duration (optional, 228 | // default=10 minutes) must be a positive integer; time unit (optional, 229 | // default=s) must be one of s, m, h, d, w; maximum duration is 2 weeks. 230 | // Combinations like 1d2h are also allowed. Reason is optional and will 231 | // be shown to the target user and other moderators. Use "untimeout" to 232 | // remove a timeout. 233 | char privmsg[TWIRC_BUFFER_SIZE]; 234 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/timeout %s %.0d %s", 235 | chan, nick, secs, reason == NULL ? "" : reason); 236 | return libtwirc_send(state, privmsg); 237 | } 238 | 239 | /* 240 | * Broadcasters and Moderators only: 241 | * Un-timeout the given user in the given channel. You could use unban as well. 242 | */ 243 | int 244 | twirc_cmd_untimeout(twirc_state_t *state, const char *chan, const char *nick) 245 | { 246 | // Usage: "/untimeout " - Removes a timeout on a user. 247 | char privmsg[TWIRC_BUFFER_SIZE]; 248 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/untimeout %s", 249 | chan, nick); 250 | return libtwirc_send(state, privmsg); 251 | } 252 | 253 | /* 254 | * Broadcasters and Moderators only: 255 | * Permanently ban the specified user from the specified channel. Optionally, a 256 | * reason can be given, which will be shown to the affected user and other 257 | * moderators. If you don't want to give a reason, set it to NULL. 258 | */ 259 | int 260 | twirc_cmd_ban(twirc_state_t *state, const char *chan, const char *nick, const char *reason) 261 | { 262 | // Usage: "/ban [reason]" - Permanently prevent a user from 263 | // chatting. Reason is optional and will be shown to the target user 264 | // and other moderators. Use "unban" to remove a ban. 265 | char privmsg[TWIRC_BUFFER_SIZE]; 266 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/ban %s %s", 267 | chan, nick, reason == NULL ? "" : reason); 268 | return libtwirc_send(state, privmsg); 269 | } 270 | 271 | /* 272 | * Broadcasters and Moderators only: 273 | * Unban the specified user from the specified channel. Also removes timeouts. 274 | */ 275 | int 276 | twirc_cmd_unban(twirc_state_t *state, const char *chan, const char *nick) 277 | { 278 | // Usage: "/unban " - Removes a ban on a user. 279 | char privmsg[TWIRC_BUFFER_SIZE]; 280 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/unban %s", 281 | chan, nick); 282 | return libtwirc_send(state, privmsg); 283 | } 284 | 285 | /* 286 | * Broadcasters and Moderators only: 287 | * Enable slow mode in the specified channel. This means users can only send 288 | * messages every `secs` seconds. If seconds is 0, the Twitch default, which at 289 | * the time of writing is 120 seconds, will be used. 290 | */ 291 | int 292 | twirc_cmd_slow(twirc_state_t *state, const char *chan, int secs) 293 | { 294 | // Usage: "/slow [duration]" - Enables slow mode (limit how often users 295 | // may send messages). Duration (optional, default=120) must be a 296 | // positive number of seconds. Use "slowoff" to disable. 297 | char privmsg[TWIRC_BUFFER_SIZE]; 298 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/slow %.0d", 299 | chan, secs); 300 | return libtwirc_send(state, privmsg); 301 | } 302 | 303 | /* 304 | * Broadcasters and Moderators only: 305 | * Disables slow mode in the specified channel. 306 | */ 307 | int 308 | twirc_cmd_slowoff(twirc_state_t *state, const char *chan) 309 | { 310 | // Usage: "/slowoff" - Disables slow mode. 311 | char privmsg[TWIRC_BUFFER_SIZE]; 312 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/slowoff", chan); 313 | return libtwirc_send(state, privmsg); 314 | } 315 | 316 | /* 317 | * Broadcasters and Moderators only: 318 | * Enables followers-only mode for the specified channel. 319 | * This means that only followers can still send messages. If `time` is set, 320 | * only followers who have been following for at least the specified time will 321 | * be allowed to chat. Allowed values range from 0 minutes (all followers) to 322 | * 3 months. `time` should be NULL or a string as in the following examples: 323 | * "7m" or "7 minutes", "2h" or "2 hours", "5d" or "5 days", "1w" or "1 week", 324 | * "3mo" or "3 months". 325 | */ 326 | int 327 | twirc_cmd_followers(twirc_state_t *state, const char *chan, const char *time) 328 | { 329 | // Usage: "/followers [duration]" - Enables followers-only mode (only 330 | // users who have followed for 'duration' may chat). Examples: "30m", 331 | // "1 week", "5 days 12 hours". Must be less than 3 months. 332 | char privmsg[TWIRC_BUFFER_SIZE]; 333 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/followers %s", 334 | chan, time == NULL ? "" : time); 335 | return libtwirc_send(state, privmsg); 336 | } 337 | 338 | /* 339 | * Broadcasters and Moderators only: 340 | * Disable followers-only mode for the specified channel. 341 | */ 342 | int 343 | twirc_cmd_followersoff(twirc_state_t *state, const char *chan) 344 | { 345 | // Usage: "/followersoff - Disables followers-only mode. 346 | char privmsg[TWIRC_BUFFER_SIZE]; 347 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/followersoff", chan); 348 | return libtwirc_send(state, privmsg); 349 | } 350 | 351 | /* 352 | * Broadcasters and Moderators only: 353 | * Enable subscriber-only mode for the specified channel. 354 | */ 355 | int 356 | twirc_cmd_subscribers(twirc_state_t *state, const char *chan) 357 | { 358 | // Usage: "/subscribers" - Enables subscribers-only mode (only subs 359 | // may chat in this channel). Use "subscribersoff" to disable. 360 | char privmsg[TWIRC_BUFFER_SIZE]; 361 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/subscribers", chan); 362 | return libtwirc_send(state, privmsg); 363 | } 364 | 365 | /* 366 | * Broadcasters and Moderators only: 367 | * Disable subscriber-only mode for the specified channel. 368 | */ 369 | int 370 | twirc_cmd_subscribersoff(twirc_state_t *state, const char *chan) 371 | { 372 | // Usage: "/subscribersoff" - Disables subscribers-only mode. 373 | char privmsg[TWIRC_BUFFER_SIZE]; 374 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/subscribersoff", chan); 375 | return libtwirc_send(state, privmsg); 376 | } 377 | 378 | /* 379 | * Broadcasters and Moderators only: 380 | * Completely wipe the previous chat history. Note: clients can ignore this. 381 | */ 382 | int 383 | twirc_cmd_clear(twirc_state_t *state, const char *chan) 384 | { 385 | // Usage: "/clear" - Clear chat history for all users in this room. 386 | char privmsg[TWIRC_BUFFER_SIZE]; 387 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/clear", chan); 388 | return libtwirc_send(state, privmsg); 389 | } 390 | 391 | /* 392 | * Broadcasters and Moderators only: 393 | * Enables R9K mode for the specified channel. 394 | * Check the Twitch docs for further information about R9K mode. 395 | */ 396 | int 397 | twirc_cmd_r9k(twirc_state_t *state, const char *chan) 398 | { 399 | // Usage: "/r9kbeta" - Enables r9k mode. Use "r9kbetaoff" to disable. 400 | char privmsg[TWIRC_BUFFER_SIZE]; 401 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/r9kbeta", chan); 402 | return libtwirc_send(state, privmsg); 403 | } 404 | 405 | /* 406 | * Broadcasters and Moderators only: 407 | * Disables R9K mode for the specified channel. 408 | */ 409 | int 410 | twirc_cmd_r9koff(twirc_state_t *state, const char *chan) 411 | { 412 | // Usage: "/r9kbetaoff" - Disables r9k mode. 413 | char privmsg[TWIRC_BUFFER_SIZE]; 414 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/r9kbetaoff", chan); 415 | return libtwirc_send(state, privmsg); 416 | } 417 | 418 | /* 419 | * Broadcasters and Moderators only: 420 | * Enables emote-only mode for the specified channel. 421 | * This means that only messages that are 100% emotes are allowed. 422 | */ 423 | int 424 | twirc_cmd_emoteonly(twirc_state_t *state, const char *chan) 425 | { 426 | // Usage: "/emoteonly" - Enables emote-only mode (only emoticons may 427 | // be used in chat). Use "emoteonlyoff" to disable. 428 | char privmsg[TWIRC_BUFFER_SIZE]; 429 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/emoteonly", chan); 430 | return libtwirc_send(state, privmsg); 431 | } 432 | 433 | /* 434 | * Broadcasters and Moderators only: 435 | * Disables emote-only mode for the specified channel. 436 | */ 437 | int 438 | twirc_cmd_emoteonlyoff(twirc_state_t *state, const char *chan) 439 | { 440 | // Usage: "/emoteonlyoff" - Disables emote-only mode. 441 | char privmsg[TWIRC_BUFFER_SIZE]; 442 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/emoteonlyoff", chan); 443 | return libtwirc_send(state, privmsg); 444 | } 445 | 446 | /* 447 | * Partner only: 448 | * Run a commercial for all viewers for `secs` seconds. `secs` can be 0, in 449 | * which case the Twitch default (30 secs at time of writing) will be used; 450 | * otherwise the following values are allowed: 30, 60, 90, 120, 150, 180. 451 | */ 452 | int 453 | twird_cmd_commercial(twirc_state_t *state, const char *chan, int secs) 454 | { 455 | char privmsg[TWIRC_BUFFER_SIZE]; 456 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/commercial %.0d", 457 | chan, secs); 458 | return libtwirc_send(state, privmsg); 459 | } 460 | 461 | /* 462 | * Broadcaster and channel editor only: 463 | * Host the channel of the user given via `target`. 464 | * Note: target channel has to be given without the pound sign ("#"). 465 | */ 466 | int 467 | twirc_cmd_host(twirc_state_t *state, const char *chan, const char *target) 468 | { 469 | char privmsg[TWIRC_BUFFER_SIZE]; 470 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/host %s", 471 | chan, target); 472 | return libtwirc_send(state, privmsg); 473 | } 474 | 475 | /* 476 | * Broadcaster and channel editor only: 477 | * Stop hosting a channel and return to the normal state. 478 | */ 479 | int 480 | twirc_cmd_unhost(twirc_state_t *state, const char *chan) 481 | { 482 | char privmsg[TWIRC_BUFFER_SIZE]; 483 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/unhost", chan); 484 | return libtwirc_send(state, privmsg); 485 | } 486 | 487 | /* 488 | * Broadcaster only: 489 | * Promote the user with the given nick to channel moderator. 490 | */ 491 | int 492 | twirc_cmd_mod(twirc_state_t *state, const char *chan, const char *nick) 493 | { 494 | char privmsg[TWIRC_BUFFER_SIZE]; 495 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/mod %s", chan, nick); 496 | return libtwirc_send(state, privmsg); 497 | } 498 | 499 | /* 500 | * Broadcaster only: 501 | * Demote the moderator with the given nick back to a regular viewer. 502 | */ 503 | int 504 | twirc_cmd_unmod(twirc_state_t *state, const char *chan, const char *nick) 505 | { 506 | char privmsg[TWIRC_BUFFER_SIZE]; 507 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/unmod %s", chan, nick); 508 | return libtwirc_send(state, privmsg); 509 | } 510 | 511 | /* 512 | * Broadcaster only: 513 | * Give VIP status to the given user in the given channel. 514 | */ 515 | int 516 | twirc_cmd_vip(twirc_state_t *state, const char *chan, const char *nick) 517 | { 518 | char privmsg[TWIRC_BUFFER_SIZE]; 519 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/vip %s", chan, nick); 520 | return libtwirc_send(state, privmsg); 521 | } 522 | 523 | /* 524 | * Broadcaster only: 525 | * Remove VIP status from the user with the given nick in the given channel. 526 | */ 527 | int 528 | twirc_cmd_unvip(twirc_state_t *state, const char *chan, const char *nick) 529 | { 530 | char privmsg[TWIRC_BUFFER_SIZE]; 531 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/unvip %s", chan, nick); 532 | return libtwirc_send(state, privmsg); 533 | } 534 | 535 | /* 536 | * Adds a stream marker at the current timestamp. Comment is optional and can 537 | * be set to NULL. If comment is used, it should not exceed 140 characters. 538 | */ 539 | int 540 | twirc_cmd_marker(twirc_state_t *state, const char *chan, const char *comment) 541 | { 542 | // Usage: "/marker" - Adds a stream marker (with an optional comment, 543 | // max 140 characters) at the current timestamp. You can use markers 544 | // in the Highlighter for easier editing. 545 | char privmsg[TWIRC_BUFFER_SIZE]; 546 | snprintf(privmsg, TWIRC_BUFFER_SIZE, "PRIVMSG %s :/marker %s", 547 | chan, comment == NULL ? "" : comment); 548 | return libtwirc_send(state, privmsg); 549 | 550 | } 551 | 552 | /* 553 | * Requests the tags capability from the Twitch server. 554 | * Returns 0 if the command was sent successfully, -1 on error. 555 | */ 556 | int 557 | twirc_cmd_req_tags(twirc_state_t *state) 558 | { 559 | return libtwirc_send(state, "CAP REQ :twitch.tv/tags"); 560 | } 561 | 562 | /* 563 | * Requests the membership capability from the Twitch server. 564 | * Returns 0 if the command was sent successfully, -1 on error. 565 | */ 566 | int 567 | twirc_cmd_req_membership(twirc_state_t *state) 568 | { 569 | return libtwirc_send(state, "CAP REQ :twitch.tv/membership"); 570 | } 571 | 572 | /* 573 | * Requests the commands capability from the Twitch server. 574 | * Returns 0 if the command was sent successfully, -1 on error. 575 | */ 576 | int 577 | twirc_cmd_req_commands(twirc_state_t *state) 578 | { 579 | return libtwirc_send(state, "CAP REQ :twitch.tv/commands"); 580 | } 581 | 582 | /* 583 | * Requests the chatrooms capability from the Twitch server. 584 | * Returns 0 if the command was sent successfully, -1 on error. 585 | */ 586 | int 587 | twirc_cmd_req_chatrooms(twirc_state_t *state) 588 | { 589 | return libtwirc_send(state, "CAP REQ :twitch.tv/tags twitch.tv/commands"); 590 | } 591 | 592 | /* 593 | * Requests the tags, membership, commands and chatrooms capabilities. 594 | * Returns 0 if the command was sent successfully, -1 on error. 595 | */ 596 | int 597 | twirc_cmd_req_all(twirc_state_t *state) 598 | { 599 | return libtwirc_send(state, 600 | "CAP REQ: twitch.tv/tags twitch.tv/commands twitch.tv/membership"); 601 | } 602 | 603 | -------------------------------------------------------------------------------- /src/libtwirc_evts.c: -------------------------------------------------------------------------------- 1 | #include // NULL, EXIT_FAILURE, EXIT_SUCCESS 2 | #include // strlen(), strerror() 3 | #include "libtwirc.h" 4 | 5 | /* 6 | * This dummy callback function does absolutely nothing. 7 | * However, it allows us to make sure that all callbacks are non-NULL, removing 8 | * the need to check for NULL everytime before we call them. It makes the code 9 | * cleaner but brings the overhead of a function call instead of a NULL-check. 10 | * I prefer this approach as long we don't run into any performance problems. 11 | * Maybe `static inline` enables the compiler to optimize the overhead. 12 | */ 13 | static inline void 14 | libtwirc_on_null(twirc_state_t *s, twirc_event_t *evt) 15 | { 16 | // Nothing in here - that's on purpose 17 | } 18 | 19 | /* 20 | * Is being called for every message we sent to the IRC server. Note that the 21 | * convenience members of the event struct ("nick", "channel", etc) will all 22 | * be NULL, as we're not looking at what kind of command/message was sent. 23 | * The raw message, as well as the raw parts ("prefix", "command", etc) will 24 | * all be available, however. 25 | */ 26 | static void 27 | libtwirc_on_outbound(twirc_state_t *s, twirc_event_t *evt) 28 | { 29 | // Nothing, otherwise we'd have to have a ton of if and else 30 | } 31 | 32 | /* 33 | * If you send an invalid command, you will get a 421 message back: 34 | * 35 | * < WHO # 36 | * > :tmi.twitch.tv 421 WHO :Unknown command 37 | */ 38 | static void 39 | libtwirc_on_invalidcmd(twirc_state_t *s, twirc_event_t *evt) 40 | { 41 | // Don't think we have to do anything here, honestly 42 | } 43 | 44 | /* 45 | * Handler for the "001" command (RPL_WELCOME), which the Twitch servers send 46 | * on successful login, even when no capabilities have been requested. 47 | */ 48 | static void 49 | libtwirc_on_welcome(twirc_state_t *s, twirc_event_t *evt) 50 | { 51 | s->status |= TWIRC_STATUS_AUTHENTICATED; 52 | } 53 | 54 | /* 55 | * On successful login. 56 | * > @badges=;color=;display-name=; 57 | * emote-sets=;turbo=;user-id=;user-type= 58 | * :tmi.twitch.tv GLOBALUSERSTATE 59 | * 60 | * badges: Comma-separated list of chat badges and the version of each badge 61 | * (each in the format /, such as admin/1). Valid 62 | * badge values: admin, bits, broadcaster, global_mod, moderator, 63 | * subscriber, staff, turbo. 64 | * color: Hexadecimal RGB color code. This is empty if it is never set. 65 | * display-name: The user’s display name, escaped as described in the IRCv3 spec. 66 | * This is empty if it is never set. 67 | * emote-sets: A comma-separated list of emotes, belonging to one or more emote 68 | * sets. This always contains at least 0. Get Chat Emoticons by Set 69 | * gets a subset of emoticons. 70 | * user-id: The user’s ID. 71 | */ 72 | static void 73 | libtwirc_on_globaluserstate(twirc_state_t *s, twirc_event_t *evt) 74 | { 75 | s->status |= TWIRC_STATUS_AUTHENTICATED; 76 | 77 | // Save the display-name and user-id in our login struct 78 | twirc_tag_t *name = twirc_get_tag_by_key(evt->tags, "display-name"); 79 | twirc_tag_t *id = twirc_get_tag_by_key(evt->tags, "user-id"); 80 | s->login.name = name ? strdup(name->value) : NULL; 81 | s->login.id = id ? strdup(id->value) : NULL; 82 | } 83 | 84 | /* 85 | * On "CAP * ACK" command, which confirms a requested capability. 86 | */ 87 | static void 88 | libtwirc_on_capack(twirc_state_t *s, twirc_event_t *evt) 89 | { 90 | // TODO Maybe we should keep track of what capabilities have been 91 | // acknowledged by the server, so the user can query it if need be? 92 | } 93 | 94 | /* 95 | * Responds to incoming PING commands with a corresponding PONG. 96 | */ 97 | static void 98 | libtwirc_on_ping(twirc_state_t *s, twirc_event_t *evt) 99 | { 100 | twirc_cmd_pong(s, evt->num_params ? evt->params[0] : NULL); 101 | } 102 | 103 | /* 104 | * When a user joins a channel we are in. The user might be us. 105 | * 106 | * > :!@.tmi.twitch.tv JOIN # 107 | */ 108 | static void 109 | libtwirc_on_join(twirc_state_t *s, twirc_event_t *evt) 110 | { 111 | if (evt->num_params > 0) 112 | { 113 | evt->channel = evt->params[0]; 114 | } 115 | } 116 | 117 | /* 118 | * Gain/lose moderator (operator) status in a channel. 119 | * 120 | * > :jtv MODE # +o 121 | * > :jtv MODE # -o 122 | * 123 | * Note: I've never actually seen this message being sent, even when 124 | * giving/revoking mod status to/from the test bot. 125 | * 126 | * Update: I've now seen this happen after joining a channel... ONCE. 127 | * It happened on the #esl_csgo channel, where after join, I first 128 | * received the NAMES list, followed by this, which happened to be 129 | * the list of all moderators in the channel at that point in time: 130 | * 131 | * > :jtv MODE #esl_csgo +o logviewer 132 | * > :jtv MODE #esl_csgo +o feralhelena 133 | * > :jtv MODE #esl_csgo +o moobot 134 | * > :jtv MODE #esl_csgo +o general23497 135 | * > :jtv MODE #esl_csgo +o xhipgamer 136 | * > :jtv MODE #esl_csgo +o xzii 137 | * > :jtv MODE #esl_csgo +o 2divine 138 | * > :jtv MODE #esl_csgo +o cent 139 | * > :jtv MODE #esl_csgo +o x_samix_x 140 | * > :jtv MODE #esl_csgo +o ravager01 141 | * > :jtv MODE #esl_csgo +o doctorwigglez 142 | */ 143 | static void 144 | libtwirc_on_mode(twirc_state_t *s, twirc_event_t *evt) 145 | { 146 | if (evt->num_params > 0) 147 | { 148 | evt->channel = evt->params[0]; 149 | } 150 | } 151 | 152 | /* 153 | * List current chatters in a channel. 154 | * If there are more than 1000 chatters in a room, NAMES return only the list 155 | * of operator privileges currently in the room. 156 | * 157 | * > :.tmi.twitch.tv 353 = # : 158 | * > :.tmi.twitch.tv 353 = # : ... 159 | * > :.tmi.twitch.tv 366 # :End of /NAMES list 160 | */ 161 | static void 162 | libtwirc_on_names(twirc_state_t *s, twirc_event_t *evt) 163 | { 164 | if (strcmp(evt->command, "353") == 0 && evt->num_params > 2) 165 | { 166 | evt->channel = evt->params[2]; 167 | return; 168 | } 169 | if (strcmp(evt->command, "366") == 0 && evt->num_params > 1) 170 | { 171 | evt->channel = evt->params[1]; 172 | return; 173 | } 174 | } 175 | 176 | /* 177 | * Depart from a channel. 178 | * 179 | * > :!@.tmi.twitch.tv PART # 180 | */ 181 | static void 182 | libtwirc_on_part(twirc_state_t *s, twirc_event_t *evt) 183 | { 184 | if (evt->num_params > 0) 185 | { 186 | evt->channel = evt->params[0]; 187 | } 188 | } 189 | 190 | /* 191 | * Temporary or permanent ban on a channel. 192 | * > @ban-duration= :tmi.twitch.tv CLEARCHAT # : 193 | * 194 | * According to actual tests, a /ban command will emit the following two messages: 195 | * > @room-id=;target-user-id=;tmi-sent-ts= 196 | * :tmi.twitch.tv CLEARCHAT # : 197 | * > @msg-id=ban_success :tmi.twitch.tv NOTICE # 198 | * : is now banned from this channel. 199 | * 200 | * Also, I've figured out that the CLEARCHAT message will also be triggered 201 | * when a mod issued the /clear command to clear the entire chat history: 202 | * > @room-id=;tmi-sent-ts= :tmi.twitch.tv CLEARCHAT # 203 | * 204 | * ban-duration: (Optional) Duration of the timeout, in seconds. 205 | * If omitted, the ban is permanent. 206 | * 207 | * Note that there is no way to figure out who banned the user. This is by 208 | * design as "users could scrape it, and use it to target the mods that timed 209 | * them out or banned them." 210 | */ 211 | static void 212 | libtwirc_on_clearchat(twirc_state_t *s, twirc_event_t *evt) 213 | { 214 | if (evt->num_params > 0) 215 | { 216 | evt->channel = evt->params[0]; 217 | } 218 | } 219 | 220 | /* 221 | * Single message removal on a channel. This is triggered via 222 | * /delete on IRC. 223 | * 224 | * > @login=;target-msg-id= 225 | * :tmi.twitch.tv CLEARMSG # : 226 | * 227 | * login: Name of the user who sent the message. 228 | * message: The message. 229 | * target-msg-id: UUID of the message. 230 | */ 231 | static void 232 | libtwirc_on_clearmsg(twirc_state_t *s, twirc_event_t *evt) 233 | { 234 | if (evt->num_params > 0) 235 | { 236 | evt->channel = evt->params[0]; 237 | } 238 | if (evt->num_params > evt->trailing) 239 | { 240 | evt->message = evt->params[evt->trailing]; 241 | } 242 | } 243 | 244 | /* 245 | * A joined channel starts or stops host mode. 246 | * 247 | * Start: 248 | * > :tmi.twitch.tv HOSTTARGET #hosting_channel [] 249 | * 250 | * Stop: 251 | * > :tmi.twitch.tv HOSTTARGET #hosting_channel :- [] 252 | * 253 | * number-of-viewers: (Optional) Number of viewers watching the host. 254 | * 255 | * Example (as seen) for host start: 256 | * > :tmi.twitch.tv HOSTTARGET #domsson :foxxwounds - 257 | * @msg-id=host_on :tmi.twitch.tv NOTICE #domsson :Now hosting foxxwounds. 258 | * 259 | * Example (as seen) for host start: 260 | * > :tmi.twitch.tv HOSTTARGET #domsson :bawnsai 0 261 | * > @msg-id=host_on :tmi.twitch.tv NOTICE #domsson :Now hosting bawnsai. 262 | * 263 | * Example (as seen) for host stop: 264 | * > :tmi.twitch.tv HOSTTARGET #domsson :- 0 265 | * > @msg-id=host_off :tmi.twitch.tv NOTICE #domsson :Exited host mode. 266 | * 267 | */ 268 | static void 269 | libtwirc_on_hosttarget(twirc_state_t *s, twirc_event_t *evt) 270 | { 271 | if (evt->num_params > 0) 272 | { 273 | evt->channel = evt->params[0]; 274 | } 275 | 276 | // If there is no trailing parameter, we exit early 277 | if (evt->num_params <= evt->trailing) 278 | { 279 | return; 280 | } 281 | 282 | // Check if there is a space in the trailing parameter 283 | char *sp = strstr(evt->params[evt->trailing], " "); 284 | if (sp == NULL) { return; } 285 | 286 | // Extract the username from the trailing parameter 287 | evt->target = strndup(evt->params[evt->trailing], 288 | sp - evt->params[evt->trailing] + 1); 289 | 290 | // If the username was "-", we set it to NULL for better indication 291 | if (strcmp(evt->target, "-") == 0) 292 | { 293 | free(evt->target); 294 | evt->target = NULL; 295 | } 296 | } 297 | 298 | /* 299 | * General notices from the server. 300 | * 301 | * > @msg-id= :tmi.twitch.tv NOTICE # : 302 | * 303 | * message: The message. 304 | * msg id: A message ID string. Can be used for i18ln. Valid values: 305 | * see Twitch IRC: msg-id Tags. 306 | * https://dev.twitch.tv/docs/irc/msg-id/ 307 | * 308 | * Example: 309 | * > @msg-id=host_target_went_offline 310 | * :tmi.twitch.tv NOTICE #domsson 311 | * :joshachu has gone offline. Exiting host mode. 312 | */ 313 | static void 314 | libtwirc_on_notice(twirc_state_t *s, twirc_event_t *evt) 315 | { 316 | if (evt->num_params > 0) 317 | { 318 | evt->channel = evt->params[0]; 319 | } 320 | if (evt->num_params > evt->trailing) 321 | { 322 | evt->message = evt->params[evt->trailing]; 323 | } 324 | } 325 | 326 | /* 327 | * CTCP ACTION 328 | */ 329 | static void 330 | libtwirc_on_action(twirc_state_t *s, twirc_event_t *evt) 331 | { 332 | if (evt->num_params > 0) 333 | { 334 | evt->channel = evt->params[0]; 335 | } 336 | if (evt->num_params > evt->trailing) 337 | { 338 | evt->message = evt->params[evt->trailing]; 339 | } 340 | } 341 | 342 | /* 343 | * Rejoin channels after a restart. 344 | * 345 | * Twitch IRC processes occasionally need to be restarted. When this happens, 346 | * clients that have requested the IRC v3 twitch.tv/commands capability are 347 | * issued a RECONNECT. After a short time, the connection is closed. In this 348 | * case, reconnect and rejoin channels that were on the connection, as you 349 | * would normally. 350 | */ 351 | static void 352 | libtwirc_on_reconnect(twirc_state_t *s, twirc_event_t *evt) 353 | { 354 | // Probably nothing to do here 355 | } 356 | 357 | /* 358 | * Send a message to a channel. 359 | * > @badges=;color=;display-name=;emotes=; 360 | * id=;mod=;room-id=;subscriber=; 361 | * tmi-sent-ts=;turbo=;user-id=;user-type= 362 | * :!@.tmi.twitch.tv PRIVMSG # : 363 | * 364 | * badges: Comma-separated list of chat badges and the version of each 365 | * badge (each in the format /, such as admin/1). 366 | * Valid badge values: admin, bits, broadcaster, global_mod, 367 | * moderator, subscriber, staff, turbo. 368 | * bits: (Sent only for Bits messages) The amount of cheer/Bits employed 369 | * by the user. All instances of these regular expressions: 370 | *  /(^\|\s)\d+(\s\|$)/ 371 | * (where is an emote name returned by the 372 | * Get Cheermotes endpoint), should be replaced with the 373 | * appropriate emote: 374 | * static-cdn.jtvnw.net/bits//// 375 | * - theme: light or dark 376 | * - type: animated or static 377 | * - color: red for 10000+ Bits, blue for 5000-9999, green for 378 | * 1000-4999, purple for 100-999, gray for 1-99 379 | * - size: A digit between 1 and 4 380 | * color: Hexadecimal RGB color code. This is empty if it is never set. 381 | * display-name: The user’s display name, escaped as described in the IRCv3 spec. 382 | * This is empty if it is never set. 383 | * emotes: Information to replace text in the message with emote images. 384 | * This can be empty. Syntax: 385 | * :-,-/:-... 386 | * - eid: The number to use in this URL: 387 | * http://static-cdn.jtvnw.net/emoticons/v1/:/: 388 | * (size is 1.0, 2.0 or 3.0.) 389 | * - f/l: Character indexes. \001ACTION does not count. 390 | * Indexing starts from the first character that is part of the 391 | * user’s actual message. 392 | * id: A unique ID for the message. 393 | * message: The message. 394 | * mod: 1 if the user has a moderator badge; otherwise, 0. 395 | * room-id: The channel ID. 396 | * tmi-sent-ts: Timestamp when the server received the message. 397 | * user-id: The user’s ID. 398 | */ 399 | static void 400 | libtwirc_on_privmsg(twirc_state_t *s, twirc_event_t *evt) 401 | { 402 | if (evt->num_params > 0) 403 | { 404 | evt->channel = evt->params[0]; 405 | } 406 | if (evt->num_params > evt->trailing) 407 | { 408 | evt->message = evt->params[evt->trailing]; 409 | } 410 | } 411 | 412 | /* 413 | * When a user joins a channel or a room setting is changed. 414 | * For a join, the message contains all chat-room settings. For changes, only the relevant tag is sent. 415 | * 416 | * > @broadcaster-lang=;emote-only=; 417 | * followers-only=;r9k=;slow=;subs-only= 418 | * :tmi.twitch.tv ROOMSTATE # 419 | * 420 | * broadcaster-lang: The chat language when broadcaster language mode is 421 | * enabled; otherwise, empty. Examples: en (English), 422 | * fi (Finnish), es-MX (Mexican variant of Spanish). 423 | * emote-only: Emote-only mode. If enabled, only emotes are allowed in 424 | * chat. Valid values: 0 (disabled) or 1 (enabled). 425 | * followers-only: Followers-only mode. If enabled, controls which followers 426 | * can chat. Valid values: -1 (disabled), 0 (all followers 427 | * can chat), or a non-negative integer (only users following 428 | * for at least the specified number of minutes can chat). 429 | * r9k: R9K mode. If enabled, messages with more than 9 characters 430 | * must be unique. Valid values: 0 (disabled) or 1 (enabled). 431 | * slow: The number of seconds chatters without moderator 432 | * privileges must wait between sending messages. 433 | * subs-only: Subscribers-only mode. If enabled, only subscribers and 434 | * moderators can chat. Valid values: 0 (disabled) or 1 435 | * (enabled). 436 | */ 437 | static void 438 | libtwirc_on_roomstate(twirc_state_t *s, twirc_event_t *evt) 439 | { 440 | if (evt->num_params > 0) 441 | { 442 | evt->channel = evt->params[0]; 443 | } 444 | } 445 | 446 | /* 447 | * On any of the following events: 448 | * - Subscription, resubscription, or gift subscription to a channel. 449 | * - Incoming raid to a channel. 450 | * - Channel ritual. Many channels have special rituals to celebrate viewer 451 | * milestones when they are shared. The rituals notice extends the sharing of 452 | * these messages to other viewer milestones (initially, a new viewer chatting 453 | * for the first time). 454 | * 455 | * These fields are sent for all USERNOTICEs: 456 | * 457 | * > @badges=;color=;display-name=;emotes=; 458 | * id=;login=;mod=;msg-id=;room-id=; 459 | * subscriber=;system-msg=;tmi-sent-ts=; 460 | * turbo=;user-id=;user-type= 461 | * :tmi.twitch.tv USERNOTICE # : 462 | * 463 | * Several other, msg-param fields are sent only for sub/resub, subgift, 464 | * anonsubgift, raid, or ritual notices. See the table below for details. 465 | * 466 | * badges: Comma-separated list of chat badges and the 467 | * version of each badge (each in the format 468 | * /, such as admin/1). Valid 469 | * badge values: admin, bits, broadcaster, 470 | * global_mod, moderator, subscriber, staff, 471 | * turbo. 472 | * color: Hexadecimal RGB color code. This is empty if 473 | * it is never set. 474 | * display-name: The user’s display name, escaped as described 475 | * in the IRCv3 spec. This is empty if it is 476 | * never set. 477 | * emotes (see PRIVMSG) 478 | * id A unique ID for the message. 479 | * login The name of the user who sent the notice. 480 | * message The message. This is omitted if the user did 481 | * not enter a message. 482 | * mod 1 if the user has a moderator badge; 483 | * otherwise, 0. 484 | * msg-id The type of notice (not the ID). Valid values: 485 | * sub, resub, subgift, anonsubgift, raid, ritual. 486 | * msg-param-cumulative-months: (Sent only on sub, resub) The total number of 487 | * months the user has subscribed. This is the 488 | * same as msg-param-months but sent for different 489 | * types of user notices. 490 | * msg-param-displayName: (Sent only on raid) The display name of the 491 | * source user raiding this channel. 492 | * msg-param-login: (Sent on only raid) The name of the source 493 | * user raiding this channel. 494 | * msg-param-months: (Sent only on subgift, anonsubgift) The total 495 | * number of months the user has subscribed. This 496 | * is the same as msg-param-cumulative-months but 497 | * sent for different types of user notices. 498 | * msg-param-recipient-display-name: (Sent only on subgift, anonsubgift) The display 499 | * name of the subscription gift recipient. 500 | * msg-param-recipient-id: (Sent only on subgift, anonsubgift) The user ID 501 | * of the subscription gift recipient. 502 | * msg-param-recipient-user-name: (Sent only on subgift, anonsubgift) The user 503 | * name of the subscription gift recipient. 504 | * msg-param-should-share-streak: (Sent only on sub, resub) Boolean indicating 505 | * whether users want their streaks to be shared. 506 | * msg-param-streak-months: (Sent only on sub, resub) The number of 507 | * consecutive months the user has subscribed. 508 | * This is 0 if msg-param-should-share-streak is 0. 509 | * msg-param-sub-plan: (Sent only on sub, resub, subgift, anonsubgift) 510 | * The type of subscription plan being used. 511 | * Valid values: Prime, 1000, 2000, 3000. 1000, 512 | * 2000, and 3000 refer to the first, second, and 513 | * third levels of paid subscriptions, respectively 514 | * (currently $4.99, $9.99, and $24.99). 515 | * msg-param-sub-plan-name (Sent only on sub, resub, subgift, anonsubgift) 516 | * The display name of the subscription plan. This 517 | * may be a default name or one created by the 518 | * channel owner. 519 | * msg-param-viewerCount: (Sent only on raid) The number of viewers 520 | * watching the source channel raiding this channel. 521 | * msg-param-ritual-name: (Sent only on ritual) The name of the ritual 522 | * this notice is for. Valid value: new_chatter. 523 | * room-id: The channel ID. 524 | * system-msg: The message printed in chat along with this notice. 525 | * tmi-sent-ts: Timestamp when the server received the message. 526 | * user-id: The user’s ID. 527 | */ 528 | static void 529 | libtwirc_on_usernotice(twirc_state_t *s, twirc_event_t *evt) 530 | { 531 | if (evt->num_params > 0) 532 | { 533 | evt->channel = evt->params[0]; 534 | } 535 | if (evt->num_params > evt->trailing) 536 | { 537 | evt->message = evt->params[evt->trailing]; 538 | } 539 | } 540 | 541 | /* 542 | * When a user joins a channel or sends a PRIVMSG to a channel. 543 | * 544 | * > @badges=;color=;display-name=; 545 | * emote-sets=;mod=;subscriber=;turbo=; 546 | * user-type= 547 | * :tmi.twitch.tv USERSTATE # 548 | * 549 | * badges: Comma-separated list of chat badges and the version of each badge 550 | * (each in the format /, such as admin/1). 551 | * Valid badge values: admin, bits, broadcaster, global_mod, 552 | * moderator, subscriber, staff, turbo, vip. 553 | * color: Hexadecimal RGB color code. This is empty if it is never set. 554 | * display-name: The user’s display name, escaped as described in the IRCv3 spec. 555 | * This is empty if it is never set. 556 | * emotes: Your emote set, a comma-separated list of emotes. This always 557 | * contains at least 0. Get Chat Emoticons by Set gets a subset of 558 | * emoticon images. 559 | * mod: 1 if the user has a moderator badge; otherwise, 0. 560 | */ 561 | static void 562 | libtwirc_on_userstate(twirc_state_t *s, twirc_event_t *evt) 563 | { 564 | if (evt->num_params > 0) 565 | { 566 | evt->channel = evt->params[0]; 567 | } 568 | } 569 | 570 | /* 571 | * This doesn't seem to be documented. Also, I've read in several places that 572 | * Twitch plans to move away from IRC for whispers (and whispers only). But for 573 | * now, sending a whisper to the bot does arrive as a regular IRC message, more 574 | * precisely as a WHISPER message. Example: 575 | * 576 | * @badges=;color=#DAA520;display-name=domsson;emotes=;message-id=7; 577 | * thread-id=65269353_274538602;turbo=0;user-id=65269353;user-type= 578 | * :domsson!domsson@domsson.tmi.twitch.tv WHISPER kaulmate :hey kaul! 579 | */ 580 | static void 581 | libtwirc_on_whisper(twirc_state_t *s, twirc_event_t *evt) 582 | { 583 | if (evt->num_params > 0) 584 | { 585 | evt->target = strdup(evt->params[0]); 586 | } 587 | if (evt->num_params > evt->trailing) 588 | { 589 | evt->message = evt->params[evt->trailing]; 590 | } 591 | } 592 | 593 | /* 594 | * Handles all events that do not (yet) have a dedicated event handler. 595 | */ 596 | static void 597 | libtwirc_on_other(twirc_state_t *s, twirc_event_t *evt) 598 | { 599 | // As we don't know what kind of event this is, we do nothing here 600 | } 601 | 602 | /* 603 | * This is not triggered by an actual IRC message, but by libtwirc once it 604 | * detects that a connection to the IRC server has been established. Hence, 605 | * there is no twirc_event struct for this callback. 606 | */ 607 | static void 608 | libtwirc_on_connect(twirc_state_t *s) 609 | { 610 | // Set status to connected (discarding all other flags) 611 | s->status = TWIRC_STATUS_CONNECTED; 612 | 613 | // Request capabilities before login, so that we will receive the 614 | // GLOBALUSERSTATE command on login in addition to the 001 (WELCOME) 615 | libtwirc_capreq(s); 616 | 617 | // Start authentication process (user login) 618 | libtwirc_auth(s); 619 | } 620 | 621 | /* 622 | * This is not triggered by an actual IRC message, but by libtwirc once it 623 | * detects that the connection to the IRC server has been lost. Hence, there 624 | * is no twirc_event struct for this callback. 625 | */ 626 | static void 627 | libtwirc_on_disconnect(twirc_state_t *s) 628 | { 629 | // Set status to disconnected (discarding all other flags) 630 | s->status = TWIRC_STATUS_DISCONNECTED; 631 | 632 | // Close the socket (this might fail as it might be closed already); 633 | // we're not checking for that error and therefore we don't report 634 | // the error via s->error for two reasons: first, we kind of expect 635 | // this to fail; second: we don't want to override more meaningful 636 | // errors that might have occurred before 637 | tcpsock_close(s->socket_fd); 638 | } 639 | 640 | -------------------------------------------------------------------------------- /src/libtwirc.c: -------------------------------------------------------------------------------- 1 | #define TCPSOCK_IMPLEMENTATION 2 | 3 | #include // NULL, fprintf(), perror() 4 | #include // NULL, EXIT_FAILURE, EXIT_SUCCESS 5 | #include // errno 6 | #include // close() 7 | #include // strlen(), strerror() 8 | #include // epoll_create(), epoll_ctl(), epoll_wait() 9 | #include // time() (as seed for rand()) 10 | #include // sigset_t et al 11 | #include "tcpsock.h" 12 | #include "libtwirc.h" 13 | #include "libtwirc_internal.h" 14 | #include "libtwirc_cmds.c" 15 | #include "libtwirc_util.c" 16 | #include "libtwirc_evts.c" 17 | 18 | /* 19 | * Sets the state's error flag to TWIRC_ERR_OUT_OF_MEMORY and returns -1. 20 | */ 21 | static int 22 | libtwirc_oom(twirc_state_t *s) 23 | { 24 | s->error = TWIRC_ERR_OUT_OF_MEMORY; 25 | return -1; 26 | } 27 | 28 | /* 29 | * Requests all supported capabilities from the Twitch servers. 30 | * Returns 0 if the request was sent successfully, -1 on error. 31 | */ 32 | static int 33 | libtwirc_capreq(twirc_state_t *s) 34 | { 35 | // TODO chatrooms cap currently not implemented! 36 | // Once that's done, use the following line 37 | // in favor of the other boongawoonga here. 38 | // return twirc_cmd_req_all(s); 39 | 40 | int success = 0; 41 | success += twirc_cmd_req_tags(s); 42 | success += twirc_cmd_req_membership(s); 43 | success += twirc_cmd_req_commands(s); 44 | return success == 0 ? 0 : -1; 45 | } 46 | 47 | /* 48 | * Authenticates with the Twitch Server using the NICK and PASS commands. 49 | * Login is not automatically completed upon return of this function, one has 50 | * to wait for the server to reply. If the tags capability has been requested 51 | * beforehand, the server will confirm login with the GLOBALUSERSTATE command, 52 | * otherwise just look out for the MOTD (starting with numeric command 001). 53 | * Returns 0 if both commands were send successfully, -1 on error. 54 | */ 55 | static int 56 | libtwirc_auth(twirc_state_t *s) 57 | { 58 | if (twirc_cmd_pass(s, s->login.pass) == -1) 59 | { 60 | return -1; 61 | } 62 | if (twirc_cmd_nick(s, s->login.nick) == -1) 63 | { 64 | return -1; 65 | } 66 | 67 | s->status |= TWIRC_STATUS_AUTHENTICATING; 68 | return 0; 69 | } 70 | 71 | static void 72 | libtwirc_free_callbacks(twirc_state_t *s) 73 | { 74 | // We do not need to free the callback pointers, as they are function 75 | // pointers and were therefore not allocated with malloc() or similar. 76 | // However, let's make sure we 'forget' the currently assigned funcs. 77 | memset(&s->cbs, 0, sizeof(twirc_callbacks_t)); 78 | } 79 | 80 | static void 81 | libtwirc_free_login(twirc_state_t *s) 82 | { 83 | free(s->login.host); 84 | free(s->login.port); 85 | free(s->login.nick); 86 | free(s->login.pass); 87 | free(s->login.name); 88 | free(s->login.id); 89 | s->login.host = NULL; 90 | s->login.port = NULL; 91 | s->login.nick = NULL; 92 | s->login.pass = NULL; 93 | s->login.name = NULL; 94 | s->login.id = NULL; 95 | } 96 | 97 | /* 98 | * Copies a portion of src to dest. The copied part will start from the offset 99 | * given in off and end at the next null terminator encountered, but at most 100 | * slen bytes. It is essential for slen to be correctly provided, as this 101 | * function is supposed to handle src strings that were network-transmitted and 102 | * might not be null-terminated. The copied string will be null terminated, even 103 | * if no null terminator was read from src. If dlen is not sufficient to store 104 | * the copied string plus the null terminator, or if the given offset points to 105 | * the end or beyond src, -1 is returned. Returns the number of bytes copied + 1, 106 | * so this value can be used as an offset for successive calls of this function, 107 | * or slen if all chunks have been read. 108 | */ 109 | static size_t 110 | libtwirc_next_chunk(char *dest, size_t dlen, const char *src, size_t slen, size_t off) 111 | { 112 | /* 113 | | recv() 1 | recv() 2 | 114 | |-------------|----------------| 115 | | SOMETHING\r | \n\0ELSE\r\n\0 | 116 | 117 | chunk 1 -> SOMETHING\0 118 | chunk 2 -> \0 119 | chunk 3 -> ELSE\0 120 | 121 | | recv(1) | recv() 2 | 122 | |-------------|---------------| 123 | | SOMETHING\r | \nSTUFF\r\n\0 | 124 | 125 | chunk 1 -> SOMETHING\0 126 | chunk 2 -> \nSTUFF\0 127 | */ 128 | 129 | // Determine the maximum number of bytes we'll have to read 130 | size_t chunk_len = slen - off; 131 | 132 | // Invalid offset (or maybe we're just done reading src) 133 | if (chunk_len <= 0) 134 | { 135 | return -1; 136 | } 137 | 138 | // Figure out the maximum size we can read 139 | size_t max_len = dlen < chunk_len ? dlen : chunk_len; 140 | 141 | // Copy everything from offset to (and including) the next null terminator 142 | // but at most max_len bytes (important if there is no null terminator) 143 | strncpy(dest, src + off, max_len); 144 | 145 | // Check how much we've actually written to the dest buffer 146 | size_t cpy_len = strnlen(dest, max_len); 147 | 148 | // Check if we've entirely filled the buffer (no space for \0) 149 | if (cpy_len == dlen) 150 | { 151 | dest[0] = '\0'; 152 | return -1; 153 | } 154 | 155 | // Make sure there is a null terminator at the end, even if 156 | // there was no null terminator in the copied string 157 | dest[cpy_len] = '\0'; 158 | 159 | // Calculate the offset to the next chunk (+1 to skip null terminator) 160 | size_t new_off = off + cpy_len + 1; 161 | 162 | // Return slen if we've read all data, otherwise the offset to the next chunk 163 | return new_off >= slen ? slen : new_off; 164 | } 165 | 166 | /* 167 | * Finds the first occurrence of the sep string in src, then copies everything 168 | * that is found before the separator into dest. The extracted part and the 169 | * separator will be removed from src. Note that this function will by design 170 | * not extract the last token, unless it also ends in the given separator. 171 | * This is so because it was designed to extract complete IRC messages from a 172 | * string containing multiple of those, where the last one might be incomplete. 173 | * Only extracting tokens that _end_ in the separator guarantees that we will 174 | * only extract complete IRC messages and leave incomplete ones in src. 175 | * 176 | * Returns the size of the copied string, or 0 if the separator could not be 177 | * found in the src string. 178 | */ 179 | static size_t 180 | libtwirc_shift_token(char *dest, char *src, const char *sep) 181 | { 182 | // Find the first occurence of the separator 183 | char *sep_pos = strstr(src, sep); 184 | if (sep_pos == NULL) 185 | { 186 | return 0; 187 | } 188 | 189 | // Figure out the length of the token etc 190 | size_t sep_len = strlen(sep); 191 | size_t new_len = strlen(sep_pos) - sep_len; 192 | size_t tok_len = sep_pos - src; 193 | 194 | // Copy the token to the dest buffer 195 | strncpy(dest, src, tok_len); 196 | dest[tok_len] = '\0'; 197 | 198 | // Remove the token from src 199 | memmove(src, sep_pos + sep_len, new_len); 200 | 201 | // Make sure src is null terminated again 202 | src[new_len] = '\0'; 203 | 204 | return tok_len; 205 | } 206 | 207 | /* 208 | * Takes an escaped string (as described in the IRCv3 spec, section tags) 209 | * and returns a pointer to a malloc'd string that holds the unescaped string. 210 | * Remember that the returned pointer has to be free'd by the caller! 211 | * TODO: THIS CAN SEGFAULT! FIX IT! 212 | * TODO: ^- regarding above comment, I think we fixed the segfault and simply 213 | * forgot to remove the comment... but not sure, so please re-check. 214 | */ 215 | static char* 216 | libtwirc_unescape(const char *str) 217 | { 218 | size_t str_len = strlen(str); 219 | char *unescaped = malloc((str_len + 1) * sizeof(char)); 220 | if (unescaped == NULL) { return NULL; } 221 | 222 | int u = 0; 223 | for (int i = 0; i < (int) str_len; ++i) 224 | { 225 | if (str[i] == '\\') 226 | { 227 | if (str[i+1] == ':') // "\:" -> ";" 228 | { 229 | unescaped[u++] = ';'; 230 | ++i; 231 | continue; 232 | } 233 | if (str[i+1] == 's') // "\s" -> " "; 234 | { 235 | unescaped[u++] = ' '; 236 | ++i; 237 | continue; 238 | } 239 | if (str[i+1] == '\\') // "\\" -> "\"; 240 | { 241 | unescaped[u++] = '\\'; 242 | ++i; 243 | continue; 244 | } 245 | if (str[i+1] == 'r') // "\r" -> '\r' (CR) 246 | { 247 | unescaped[u++] = '\r'; 248 | ++i; 249 | continue; 250 | } 251 | if (str[i+1] == 'n') // "\n" -> '\n' (LF) 252 | { 253 | unescaped[u++] = '\n'; 254 | ++i; 255 | continue; 256 | } 257 | } 258 | unescaped[u++] = str[i]; 259 | } 260 | unescaped[u] = '\0'; 261 | return unescaped; 262 | } 263 | 264 | /* 265 | * Dynamically allocates a twirc_tag_t from the given key and value strings. 266 | * Returns NULL if memory allocation failed or the given key was NULL or an 267 | * empty string, otherwise a pointer to the created tag. If the given value 268 | * was NULL, it will be set to an empty string. Both key and value are also 269 | * dynamically allocated and need to be free'd by the caller at some point. 270 | */ 271 | static twirc_tag_t* 272 | libtwirc_create_tag(const char *key, const char *val) 273 | { 274 | // Key can't be NULL or empty 275 | if (key == NULL || strlen(key) == 0) 276 | { 277 | return NULL; 278 | } 279 | 280 | // Allocate memory 281 | twirc_tag_t *tag = malloc(sizeof(twirc_tag_t)); 282 | 283 | // Check if allocation worked 284 | if (tag == NULL) { 285 | return NULL; 286 | } 287 | 288 | // Set key and value; if value was NULL, set it to empty string 289 | tag->key = strdup(key); 290 | tag->value = val == NULL ? strdup("") : libtwirc_unescape(val); 291 | 292 | return tag; 293 | } 294 | 295 | static void 296 | libtwirc_free_tag(twirc_tag_t *tag) 297 | { 298 | free(tag->key); 299 | free(tag->value); 300 | } 301 | 302 | static void 303 | libtwirc_free_tags(twirc_tag_t **tags) 304 | { 305 | if (tags == NULL) 306 | { 307 | return; 308 | } 309 | for (int i = 0; tags[i] != NULL; ++i) 310 | { 311 | libtwirc_free_tag(tags[i]); 312 | free(tags[i]); 313 | tags[i] = NULL; 314 | } 315 | } 316 | 317 | static void 318 | libtwirc_free_params(char **params) 319 | { 320 | if (params == NULL) 321 | { 322 | return; 323 | } 324 | for (int i = 0; params[i] != NULL; ++i) 325 | { 326 | free(params[i]); 327 | params[i] = NULL; 328 | } 329 | //free(params); 330 | } 331 | 332 | /* 333 | * Extracts the nickname from an IRC message's prefix, if any. Done this way: 334 | * Searches prefix for an exclamation mark ('!'). If there is one, everything 335 | * before it will be returned as a pointer to an allocated string (malloc), so 336 | * the caller has to free() it at some point. If there is no exclamation mark 337 | * in prefix or prefix is NULL or we're out of memory, NULL will be returned. 338 | */ 339 | static char* 340 | libtwirc_parse_nick(const char *prefix) 341 | { 342 | // Nothing to do if nothing has been handed in 343 | if (prefix == NULL) 344 | { 345 | return NULL; 346 | } 347 | 348 | // Search for an exclamation mark in prefix 349 | char *sep = strstr(prefix, "!"); 350 | if (sep == NULL) 351 | { 352 | return NULL; 353 | } 354 | 355 | // Return the nick as malloc'd string 356 | size_t len = sep - prefix; 357 | return strndup(prefix, len); 358 | } 359 | 360 | /* 361 | * Extracts tags from the beginning of an IRC message, if any, and returns them 362 | * as a pointer to a dynamically allocated array of twirc_tag structs, where 363 | * each struct contains two members, key and value, representing the key and 364 | * value of a tag, respectively. The value member of a tag can be empty string 365 | * for key-only tags. The last element of the array will be a NULL pointer, so 366 | * you can loop over all tags until you hit NULL. The number of extracted tags 367 | * is returned in len. If no tags have been found at the beginning of msg, tags 368 | * will be NULL, len will be 0 and this function will return a pointer to msg. 369 | * Otherwise, a pointer to the part of msg after the tags will be returned. 370 | * 371 | * https://ircv3.net/specs/core/message-tags-3.2.html 372 | */ 373 | static const char* 374 | libtwirc_parse_tags(const char *msg, twirc_tag_t ***tags, size_t *len) 375 | { 376 | // If msg doesn't start with "@", then there are no tags 377 | if (msg[0] != '@') 378 | { 379 | *len = 0; 380 | *tags = NULL; 381 | return msg; 382 | } 383 | 384 | // Find the next space (the end of the tags string within msg) 385 | char *next = strstr(msg, " "); 386 | 387 | // Duplicate the string from after the '@' until the next space 388 | char *tag_str = strndup(msg + 1, next - (msg + 1)); 389 | 390 | // Set the initial number of tags we want to allocate memory for 391 | size_t num_tags = TWIRC_NUM_TAGS; 392 | 393 | // Allocate memory in the provided pointer to ptr-to-array-of-structs 394 | *tags = malloc(num_tags * sizeof(twirc_tag_t*)); 395 | 396 | char *tag = NULL; 397 | int i; 398 | for (i = 0; (tag = strtok(i == 0 ? tag_str : NULL, ";")) != NULL; ++i) 399 | { 400 | // Make sure we have enough space; last element has to be NULL 401 | if (i >= num_tags - 1) 402 | { 403 | size_t add = (size_t) (num_tags / 2); 404 | num_tags += add; 405 | *tags = realloc(*tags, num_tags * sizeof(twirc_tag_t*)); 406 | } 407 | 408 | char *eq = strstr(tag, "="); 409 | 410 | // It's a key-only tag, like "foo" (never seen that on Twitch) 411 | // Hence, we didn't find a '=' at all 412 | if (eq == NULL) 413 | { 414 | // TODO we should check for libtwirc_create_tag() 415 | // returning NULL and act accordingly 416 | (*tags)[i] = libtwirc_create_tag(tag, NULL); 417 | } 418 | // It's either a key-only tag with a trailing '=' ("foo=") 419 | // or a tag with key-value pair, like "foo=bar" 420 | else 421 | { 422 | // Turn the '=' into '\0' to separate key and value 423 | eq[0] = '\0'; // Turn the '=' into '\0' 424 | 425 | // TODO we should check for libtwirc_create_tag() 426 | // returning NULL and act accordingly 427 | (*tags)[i] = libtwirc_create_tag(tag, eq+1); 428 | } 429 | 430 | //fprintf(stderr, ">>> TAG %d: %s = %s\n", i, (*tags)[i]->key, (*tags)[i]->value); 431 | } 432 | 433 | // Set the number of tags found 434 | *len = i; 435 | 436 | free(tag_str); 437 | 438 | // Trim this down to the exact right size 439 | if (i < num_tags - 1) 440 | { 441 | *tags = realloc(*tags, (i + 1) * sizeof(twirc_tag_t*)); 442 | } 443 | 444 | // Make sure the last element is a NULL ptr 445 | (*tags)[i] = NULL; 446 | 447 | // Return a pointer to the remaining part of msg 448 | return next + 1; 449 | } 450 | 451 | /* 452 | * Extracts the prefix from the beginning of msg, if there is one. The prefix 453 | * will be returned as a pointer to a dynamically allocated string in prefix. 454 | * The caller needs to make sure to free the memory at some point. If no prefix 455 | * was found at the beginning of msg, prefix will be NULL. Returns a pointer to 456 | * the next part of the message, after the prefix. 457 | */ 458 | static const char* 459 | libtwirc_parse_prefix(const char *msg, char **prefix) 460 | { 461 | if (msg[0] != ':') 462 | { 463 | *prefix = NULL; 464 | return msg; 465 | } 466 | 467 | // Find the next space (the end of the prefix string within msg) 468 | char *next = strstr(msg, " "); 469 | 470 | // Duplicate the string from after the ':' until the next space 471 | *prefix = strndup(msg + 1, next - (msg + 1)); 472 | 473 | // Return a pointer to the remaining part of msg 474 | return next + 1; 475 | } 476 | 477 | static const char* 478 | libtwirc_parse_command(const char *msg, char **cmd) 479 | { 480 | // Find the next space (the end of the cmd string withing msg) 481 | char *next = strstr(msg, " "); 482 | 483 | // Duplicate the string from start to next space or end of msg 484 | *cmd = strndup(msg, next == NULL ? strlen(msg) : next - msg); 485 | 486 | // Return NULL if cmd was the last bit of msg, or a pointer to 487 | // the remaining part (the parameters) 488 | return next == NULL ? NULL : next + 1; 489 | } 490 | 491 | static const char* 492 | libtwirc_parse_params(const char *msg, char ***params, size_t *len, int *t_idx) 493 | { 494 | if (msg == NULL) 495 | { 496 | *t_idx = -1; 497 | *len = 0; 498 | *params = NULL; 499 | return NULL; 500 | } 501 | 502 | // Initialize params to hold TWIRC_NUM_PARAMS pointers to char 503 | size_t num_params = TWIRC_NUM_PARAMS; 504 | *params = malloc(num_params * sizeof(char*)); 505 | 506 | // Copy everything that's left in msg (params are always last) 507 | char *p_str = strdup(msg); 508 | 509 | size_t p_len = strlen(p_str); 510 | size_t num_tokens = 0; 511 | int trailing = 0; 512 | int from = 0; 513 | 514 | for (int i = 0; i < p_len; ++i) 515 | { 516 | // Make sure we have enough space; last element has to be NULL 517 | if (num_tokens >= num_params - 1) 518 | { 519 | size_t add = num_params; 520 | num_params += add; 521 | *params = realloc(*params, num_params * sizeof(char*)); 522 | } 523 | 524 | // Prefix of trailing token (ignore if part of trailing token) 525 | if (p_str[i] == ':' && !trailing) 526 | { 527 | // Remember that we're in the trailing token 528 | trailing = 1; 529 | // Set the start position marker to the next char 530 | from = i+1; 531 | continue; 532 | } 533 | 534 | // Token separator (ignore if trailing token) 535 | if (p_str[i] == ' ' && !trailing) 536 | { 537 | // Copy everything from the beginning to here 538 | (*params)[num_tokens++] = strndup(p_str + from, i - from); 539 | //fprintf(stderr, "- %s\n", (*params)[num_tokens-1]); 540 | // Set the start position marker to the next char 541 | from = i+1; 542 | continue; 543 | } 544 | 545 | // We're at the last character (null terminator, hopefully) 546 | if (i == p_len - 1) 547 | { 548 | // Copy everything from the beginning to here + 1 549 | (*params)[num_tokens++] = strndup(p_str + from, (i + 1) - from); 550 | //fprintf(stderr, "- %s\n", (*params)[num_tokens-1]); 551 | } 552 | } 553 | 554 | // Set index of the trailing parameter, if any, otherwise -1 555 | *t_idx = trailing ? num_tokens - 1 : -1; 556 | // Set number of tokens (parameters) found and copied 557 | *len = num_tokens; 558 | 559 | free(p_str); 560 | 561 | // Trim this down to the exact amount of memory we need 562 | if (num_tokens < num_params - 1) 563 | { 564 | *params = realloc(*params, (num_tokens + 1) * sizeof(char*)); 565 | } 566 | 567 | // Make sure the last element is a NULL ptr 568 | (*params)[num_tokens] = NULL; 569 | 570 | // We've reached the end of msg, so we'll return NULL 571 | return NULL; 572 | } 573 | 574 | /* 575 | * Checks if the event is a CTCP event. If so, strips the CTCP markers (0x01) 576 | * as well as the CTCP command from the trailing parameter and fills the ctcp 577 | * member of evt with the CTCP command instead. If it isn't a CTCP command, 578 | * this function does nothing. 579 | * Returns 0 on success, -1 if an error occured (not enough memory). 580 | */ 581 | static int 582 | libtwirc_parse_ctcp(twirc_event_t *evt) 583 | { 584 | // Can't be CTCP if we don't even have enough parameters 585 | if (evt->num_params <= evt->trailing) 586 | { 587 | return 0; 588 | } 589 | 590 | // For convenience, get a ptr to the trailing parameter 591 | char *trailing = evt->params[evt->trailing]; 592 | 593 | // First char not 0x01? Not CTCP! 594 | if (trailing[0] != 0x01) 595 | { 596 | return 0; 597 | } 598 | 599 | // Last char not 0x01? Not CTCP! 600 | int last = strlen(trailing) - 1; 601 | if (trailing[last] != 0x01) 602 | { 603 | return 0; 604 | } 605 | 606 | // Find the first space within the trailing parameter 607 | char *space = strstr(trailing, " "); 608 | if (space == NULL) { return -1; } 609 | 610 | // Copy the CTCP command 611 | evt->ctcp = strndup(trailing + 1, space - (trailing + 1)); 612 | if (evt->ctcp == NULL) { return -1; } 613 | 614 | // Strip 0x01 615 | char *message = strndup(space + 1, (trailing + last) - (space + 1)); 616 | if (message == NULL) { return -1; } 617 | 618 | // Free the old trailing parameter, swap in the stripped one 619 | free(evt->params[evt->trailing]); 620 | evt->params[evt->trailing] = message; 621 | 622 | return 0; 623 | } 624 | 625 | static void 626 | libtwirc_dispatch_out(twirc_state_t *s, twirc_event_t *evt) 627 | { 628 | libtwirc_on_outbound(s, evt); 629 | s->cbs.outbound(s, evt); 630 | } 631 | 632 | /* 633 | * Dispatches the internal and external event handler / callback functions 634 | * for the given event, based on the command field of evt. Does not handle 635 | * CTCP events - call libtwirc_dispatch_ctcp() for those instead. 636 | */ 637 | static void 638 | libtwirc_dispatch_evt(twirc_state_t *s, twirc_event_t *evt) 639 | { 640 | // TODO try ordering these by "probably usually most frequent", so that 641 | // we waste as little CPU cycles as possible on strcmp() here! 642 | 643 | if (strcmp(evt->command, "PRIVMSG") == 0) 644 | { 645 | libtwirc_on_privmsg(s, evt); 646 | s->cbs.privmsg(s, evt); 647 | return; 648 | } 649 | if (strcmp(evt->command, "JOIN") == 0) 650 | { 651 | libtwirc_on_join(s, evt); 652 | s->cbs.join(s, evt); 653 | return; 654 | } 655 | if (strcmp(evt->command, "CLEARCHAT") == 0) 656 | { 657 | libtwirc_on_clearchat(s, evt); 658 | s->cbs.clearchat(s, evt); 659 | return; 660 | } 661 | if (strcmp(evt->command, "CLEARMSG") == 0) 662 | { 663 | libtwirc_on_clearmsg(s, evt); 664 | s->cbs.clearmsg(s, evt); 665 | return; 666 | } 667 | if (strcmp(evt->command, "NOTICE") == 0) 668 | { 669 | libtwirc_on_notice(s, evt); 670 | s->cbs.notice(s, evt); 671 | return; 672 | } 673 | if (strcmp(evt->command, "ROOMSTATE") == 0) 674 | { 675 | libtwirc_on_roomstate(s, evt); 676 | s->cbs.roomstate(s, evt); 677 | return; 678 | } 679 | if (strcmp(evt->command, "USERSTATE") == 0) 680 | { 681 | libtwirc_on_userstate(s, evt); 682 | s->cbs.userstate(s, evt); 683 | return; 684 | } 685 | if (strcmp(evt->command, "USERNOTICE") == 0) 686 | { 687 | libtwirc_on_usernotice(s, evt); 688 | s->cbs.usernotice(s, evt); 689 | return; 690 | } 691 | if (strcmp(evt->command, "WHISPER") == 0) 692 | { 693 | libtwirc_on_whisper(s, evt); 694 | s->cbs.whisper(s, evt); 695 | return; 696 | } 697 | if (strcmp(evt->command, "PART") == 0) 698 | { 699 | libtwirc_on_part(s, evt); 700 | s->cbs.join(s, evt); 701 | return; 702 | } 703 | if (strcmp(evt->command, "PING") == 0) 704 | { 705 | libtwirc_on_ping(s, evt); 706 | s->cbs.ping(s, evt); 707 | return; 708 | } 709 | if (strcmp(evt->command, "MODE") == 0) 710 | { 711 | libtwirc_on_mode(s, evt); 712 | s->cbs.mode(s, evt); 713 | return; 714 | } 715 | if (strcmp(evt->command, "353") == 0 || 716 | strcmp(evt->command, "366") == 0) 717 | { 718 | libtwirc_on_names(s, evt); 719 | s->cbs.names(s, evt); 720 | return; 721 | } 722 | if (strcmp(evt->command, "HOSTTARGET") == 0) 723 | { 724 | libtwirc_on_hosttarget(s, evt); 725 | s->cbs.hosttarget(s, evt); 726 | return; 727 | } 728 | if (strcmp(evt->command, "CAP") == 0 && 729 | strcmp(evt->params[0], "*") == 0) 730 | { 731 | libtwirc_on_capack(s, evt); 732 | s->cbs.capack(s, evt); 733 | return; 734 | } 735 | if (strcmp(evt->command, "001") == 0) 736 | { 737 | libtwirc_on_welcome(s, evt); 738 | s->cbs.welcome(s, evt); 739 | return; 740 | } 741 | if (strcmp(evt->command, "GLOBALUSERSTATE") == 0) 742 | { 743 | libtwirc_on_globaluserstate(s, evt); 744 | s->cbs.globaluserstate(s, evt); 745 | return; 746 | } 747 | if (strcmp(evt->command, "421") == 0) 748 | { 749 | libtwirc_on_invalidcmd(s, evt); 750 | s->cbs.invalidcmd(s, evt); 751 | return; 752 | } 753 | if (strcmp(evt->command, "RECONNECT") == 0) 754 | { 755 | libtwirc_on_reconnect(s, evt); 756 | s->cbs.reconnect(s, evt); 757 | return; 758 | } 759 | 760 | // Some unaccounted-for event occured 761 | libtwirc_on_other(s, evt); 762 | s->cbs.other(s, evt); 763 | } 764 | 765 | /* 766 | * Dispatches the internal and external event handler / callback functions 767 | * for the given CTCP event, based on the ctcp field of evt. Does not handle 768 | * regular events - call libtwirc_dispatch_evt() for those instead. 769 | */ 770 | static void 771 | libtwirc_dispatch_ctcp(twirc_state_t *s, twirc_event_t *evt) 772 | { 773 | if (strcmp(evt->ctcp, "ACTION") == 0) 774 | { 775 | libtwirc_on_action(s, evt); 776 | s->cbs.action(s, evt); 777 | return; 778 | } 779 | 780 | // Some unaccounted-for event occured 781 | libtwirc_on_other(s, evt); 782 | s->cbs.other(s, evt); 783 | } 784 | 785 | /* 786 | * Takes a raw IRC message and parses all the relevant information into a 787 | * twirc_event struct, then calls upon the functions responsible for the 788 | * dispatching of the event to internal and external callback functions. 789 | * Returns 0 on success, -1 if an out of memory error occured during the 790 | * parsing/handling of a CTCP event. 791 | */ 792 | static int 793 | libtwirc_process_msg(twirc_state_t *s, const char *msg, int outbound) 794 | { 795 | //fprintf(stderr, "> %s (%zu)\n", msg, strlen(msg)); 796 | 797 | int err = 0; 798 | twirc_event_t evt = { 0 }; 799 | 800 | evt.raw = strdup(msg); 801 | 802 | // Extract the tags, if any 803 | msg = libtwirc_parse_tags(msg, &(evt.tags), &(evt.num_tags)); 804 | 805 | // Extract the prefix, if any 806 | msg = libtwirc_parse_prefix(msg, &(evt.prefix)); 807 | 808 | // Extract the command, always 809 | msg = libtwirc_parse_command(msg, &(evt.command)); 810 | 811 | // Extract the parameters, if any 812 | msg = libtwirc_parse_params(msg, &(evt.params), &(evt.num_params), &(evt.trailing)); 813 | 814 | // Check for CTCP and possibly modify the event accordingly 815 | err = libtwirc_parse_ctcp(&evt); 816 | 817 | // Extract the nick from the prefix, maybe 818 | evt.origin = libtwirc_parse_nick(evt.prefix); 819 | 820 | if (outbound) 821 | { 822 | libtwirc_dispatch_out(s, &evt); 823 | } 824 | else if (evt.ctcp) 825 | { 826 | libtwirc_dispatch_ctcp(s, &evt); 827 | } 828 | else 829 | { 830 | libtwirc_dispatch_evt(s, &evt); 831 | } 832 | 833 | // Free event 834 | // TODO: make all of this into a function? libtwirc_free_event() 835 | libtwirc_free_params(evt.params); 836 | free(evt.params); 837 | evt.params = NULL; 838 | libtwirc_free_tags(evt.tags); 839 | free(evt.tags); 840 | evt.tags = NULL; 841 | free(evt.raw); 842 | free(evt.prefix); 843 | free(evt.origin); 844 | free(evt.target); 845 | free(evt.command); 846 | free(evt.ctcp); 847 | 848 | return err; 849 | } 850 | 851 | /* 852 | * Process the raw IRC data stored in `buf`, which has a size of len bytes. 853 | * Incomplete commands will be buffered in state->buffer, complete commands 854 | * will be processed right away. Returns 0 on success, -1 if out of memory. 855 | */ 856 | int 857 | libtwirc_process_data(twirc_state_t *s, const char *buf, size_t len) 858 | { 859 | /* 860 | +----------------------+----------------+ 861 | | recv() 1 | recv() 2 | 862 | +----------------------+----------------+ 863 | |USER MYNAME\r\n\0PASSW|ORD MYPASS\r\n\0| 864 | +----------------------+----------------+ 865 | 866 | -> libtwirc_next_chunk() 1.1 => "USER MYNAME\r\n\0" 867 | -> libtwirc_next_chunk() 1.2 => "PASSW\0" 868 | -> libtwirc_next_chunk() 2.1 => "ORD MYPASS\r\n\0" 869 | */ 870 | 871 | // A chunk has to be able to hold at least as much data as the buffer 872 | // we're working on. This means we'll dynamically allocate the chunk 873 | // buffer to the same size as the handed in buffer, plus one to make 874 | // room for a null terminator which might not be present in the data 875 | // received, but will definitely be added in the chunk. 876 | 877 | char *chunk = malloc(len + 1); 878 | if (chunk == NULL) { return libtwirc_oom(s); } 879 | chunk[0] = '\0'; 880 | int off = 0; 881 | 882 | // Here, we'll get one chunk at a time, where a chunk is a part of the 883 | // recieved bytes that ends in a null terminator. We'll add all of the 884 | // extracted chunks to the buffer, which might already contain parts of 885 | // an incomplete IRC command. The buffer needs to be sufficiently big 886 | // to contain more than one delivery in case one of them is incomplete, 887 | // but the next one is complete and adds the missing pieces of the 888 | // previous one. 889 | 890 | while ((off = libtwirc_next_chunk(chunk, len + 1, buf, len, off)) > 0) 891 | { 892 | // Concatenate the current buffer and the newly extracted chunk 893 | strcat(s->buffer, chunk); 894 | } 895 | 896 | free(chunk); 897 | 898 | // Here, we're lookin at each IRC command in the buffer. We do so by 899 | // getting the string from the beginning of the buffer that ends in 900 | // '\r\n', if any. This string will then be deleted from the buffer. 901 | // We do this repeatedly until we've extracted all complete commands 902 | // from the buffer. If the last bit of the buffer was an incomplete 903 | // command (did not end in '\r\n'), it will be left in the buffer. 904 | // Hopefully, successive recv() calls will bring in the missing pieces. 905 | // If not, we will run into issues, as the buffer could silently and 906 | // slowly fill up until it finally overflows. TODO: test for that? 907 | 908 | char msg[TWIRC_MESSAGE_SIZE]; 909 | msg[0] = '\0'; 910 | 911 | while (libtwirc_shift_token(msg, s->buffer, "\r\n") > 0) 912 | { 913 | // Process the message and check if we ran out of memory doing so 914 | if (libtwirc_process_msg(s, msg, 0) == -1) 915 | { 916 | return -1; 917 | } 918 | } 919 | 920 | return 0; 921 | } 922 | 923 | /* 924 | * Handles the epoll event epev. 925 | * Returns 0 on success, -1 if the connection has been interrupted or 926 | * not enough memory was available to process the incoming data. 927 | */ 928 | static int 929 | libtwirc_handle_event(twirc_state_t *s, struct epoll_event *epev) 930 | { 931 | // We've got data coming in 932 | if(epev->events & EPOLLIN) 933 | { 934 | char buf[TWIRC_BUFFER_SIZE]; 935 | int bytes_received = 0; 936 | 937 | // Fetch and process all available data from the socket 938 | while ((bytes_received = libtwirc_recv(s, buf, TWIRC_BUFFER_SIZE)) > 0) 939 | { 940 | // Process the data and check if we ran out of memory doing so 941 | if (libtwirc_process_data(s, buf, bytes_received) == -1) 942 | { 943 | s->error = TWIRC_ERR_OUT_OF_MEMORY; 944 | return -1; 945 | } 946 | } 947 | 948 | // If twirc_recv() returned -1, the connection is probably down, 949 | // either way, we have a serious issue and should stop running! 950 | if (bytes_received == -1) 951 | { 952 | s->error = TWIRC_ERR_SOCKET_RECV; 953 | 954 | // We were connected but now seem to be disconnected? 955 | if (twirc_is_connected(s) && tcpsock_status(s->socket_fd) == -1) 956 | { 957 | // If so, call the disconnect event handlers 958 | libtwirc_on_disconnect(s); 959 | s->cbs.disconnect(s, NULL); 960 | } 961 | return -1; 962 | } 963 | } 964 | 965 | // We're ready to send data 966 | if (epev->events & EPOLLOUT) 967 | { 968 | // If we weren't connected yet, we seem to be now! 969 | if (s->status & TWIRC_STATUS_CONNECTING) 970 | { 971 | // The internal connect event handler will initiate the 972 | // request of capabilities as well as the login process 973 | libtwirc_on_connect(s); 974 | s->cbs.connect(s, NULL); 975 | } 976 | } 977 | 978 | // Server closed the connection 979 | if (epev->events & EPOLLRDHUP) 980 | { 981 | s->error = TWIRC_ERR_CONN_CLOSED; 982 | libtwirc_on_disconnect(s); 983 | s->cbs.disconnect(s, NULL); 984 | return -1; 985 | } 986 | 987 | // Unexpected hangup on socket 988 | if (epev->events & EPOLLHUP) // fires even if not added explicitly 989 | { 990 | s->error = TWIRC_ERR_CONN_HANGUP; 991 | libtwirc_on_disconnect(s); 992 | s->cbs.disconnect(s, NULL); 993 | return -1; 994 | } 995 | 996 | // Socket error 997 | if (epev->events & EPOLLERR) // fires even if not added explicitly 998 | { 999 | s->error = TWIRC_ERR_CONN_SOCKET; 1000 | libtwirc_on_disconnect(s); 1001 | s->cbs.disconnect(s, NULL); 1002 | return -1; 1003 | } 1004 | 1005 | // Handled everything and no disconnect/error occurred 1006 | return 0; 1007 | } 1008 | 1009 | /* 1010 | * Sends data to the IRC server, using the state's socket. 1011 | * On success, returns the number of bytes sent. 1012 | * On error, -1 is returned and errno is set appropriately. 1013 | */ 1014 | static int 1015 | libtwirc_send(twirc_state_t *s, const char *msg) 1016 | { 1017 | // Get the actual message length (without null terminator) 1018 | // If the message is too big for the message buffer, we only 1019 | // grab as much as we can fit in our buffer (we truncate) 1020 | size_t msg_len = strnlen(msg, TWIRC_BUFFER_SIZE - 3); 1021 | 1022 | // Create a perfectly sized buffer (max TWIRC_BUFFER_SIZE) 1023 | size_t buf_len = msg_len + 3; 1024 | char *buf = malloc(buf_len * sizeof(char)); 1025 | 1026 | // Copy the user's message into our slightly larger buffer 1027 | snprintf(buf, buf_len, "%s", msg); 1028 | 1029 | // Use the additional space for line and null terminators 1030 | // IRC messages need to be CR-LF (\r\n) terminated! 1031 | buf[msg_len+0] = '\r'; 1032 | buf[msg_len+1] = '\n'; 1033 | buf[msg_len+2] = '\0'; 1034 | 1035 | // Actually send the message 1036 | int ret = tcpsock_send(s->socket_fd, buf, buf_len); 1037 | 1038 | // Dispatch the outgoing event 1039 | libtwirc_process_msg(s, msg, 1); 1040 | 1041 | free(buf); 1042 | return ret; 1043 | } 1044 | 1045 | /* 1046 | * Reads data from the socket and copies it into the provided buffer `buf`. 1047 | * Returns the number of bytes read or 0 if there was no more data to read. 1048 | * If an error occured, -1 will be returned (check errno); this usually means 1049 | * the connection has been lost or some error has occurred on the socket. 1050 | */ 1051 | static int 1052 | libtwirc_recv(twirc_state_t *s, char *buf, size_t len) 1053 | { 1054 | // Receive data 1055 | ssize_t res_len; 1056 | res_len = tcpsock_receive(s->socket_fd, buf, len - 1); 1057 | 1058 | // Check if tcpsno_receive() reported an error 1059 | if (res_len == -1) 1060 | { 1061 | if (errno == EAGAIN || errno == EWOULDBLOCK) 1062 | { 1063 | // Simply no more data to read right now - all good 1064 | return 0; 1065 | } 1066 | // Every other error, however, indicates some serious problem 1067 | return -1; 1068 | } 1069 | 1070 | // Make sure that the received data is null terminated 1071 | buf[res_len] = '\0'; 1072 | 1073 | // Return the number of bytes received 1074 | return res_len; 1075 | } 1076 | 1077 | /* 1078 | * Ininitates an anonymous connection with the given server. 1079 | * The username will be `justinfan` plus a randomly generated numeric suffix. 1080 | * Returns 0 if the connection process has started and is now in progress, 1081 | * -1 if the connection attempt failed (check the state's error and errno). 1082 | */ 1083 | int 1084 | twirc_connect_anon(twirc_state_t *s, const char *host, const char *port) 1085 | { 1086 | int r = rand() % (10 * TWIRC_USER_ANON_MAX_DIGITS); 1087 | size_t len = (strlen(TWIRC_USER_ANON) + TWIRC_USER_ANON_MAX_DIGITS + 1); 1088 | 1089 | char *anon = malloc(len * sizeof(char)); 1090 | if (anon == NULL) { return libtwirc_oom(s); } 1091 | snprintf(anon, len, "%s%d", TWIRC_USER_ANON, r); 1092 | 1093 | int res = twirc_connect(s, host, port, anon, "null"); 1094 | free(anon); 1095 | return res; 1096 | } 1097 | 1098 | /* 1099 | * Initiates a connection with the given server using the given credentials. 1100 | * Returns 0 if the connection process has started and is now in progress, 1101 | * -1 if the connection attempt failed (check the state's error and errno). 1102 | */ 1103 | int 1104 | twirc_connect(twirc_state_t *s, const char *host, const char *port, const char *nick, const char *pass) 1105 | { 1106 | // Create socket 1107 | s->socket_fd = tcpsock_create(s->ip_type, TCPSOCK_NONBLOCK); 1108 | if (s->socket_fd < 0) 1109 | { 1110 | s->error = TWIRC_ERR_SOCKET_CREATE; 1111 | return -1; 1112 | } 1113 | 1114 | // Create epoll instance 1115 | s->epfd = epoll_create(1); 1116 | if (s->epfd < 0) 1117 | { 1118 | s->error = TWIRC_ERR_EPOLL_CREATE; 1119 | return -1; 1120 | } 1121 | 1122 | // Set up the epoll instance 1123 | struct epoll_event eev = { 0 }; 1124 | eev.data.ptr = s; 1125 | eev.events = EPOLLRDHUP | EPOLLOUT | EPOLLIN | EPOLLET; 1126 | int epctl_result = epoll_ctl(s->epfd, EPOLL_CTL_ADD, s->socket_fd, &eev); 1127 | 1128 | if (epctl_result) 1129 | { 1130 | // Socket could not be registered for IO 1131 | s->error = TWIRC_ERR_EPOLL_CTL; 1132 | return -1; 1133 | } 1134 | 1135 | // Properly initialize the login struct and copy the login data into it 1136 | s->login.host = strdup(host); 1137 | s->login.port = strdup(port); 1138 | s->login.nick = strdup(nick); 1139 | s->login.pass = strdup(pass); 1140 | 1141 | // Connect the socket (and handle a possible connection error) 1142 | if (tcpsock_connect(s->socket_fd, s->ip_type, host, port) == -1) 1143 | { 1144 | s->error = TWIRC_ERR_SOCKET_CONNECT; 1145 | return -1; 1146 | } 1147 | 1148 | // We are in the process of connecting! 1149 | s->status = TWIRC_STATUS_CONNECTING; 1150 | return 0; 1151 | } 1152 | 1153 | /* 1154 | * Sends the QUIT command to the server, then terminates the connection and 1155 | * calls both the internal as well as external disconnect event handlers. 1156 | * Returns 0 on success, -1 if the socket could not be closed (see errno). 1157 | */ 1158 | int 1159 | twirc_disconnect(twirc_state_t *s) 1160 | { 1161 | // Say bye-bye to the IRC server 1162 | twirc_cmd_quit(s); 1163 | 1164 | // Close the socket and return if that worked 1165 | return tcpsock_close(s->socket_fd); 1166 | 1167 | // Note that we are NOT calling the disconnect event handlers from 1168 | // here; this is on purpose! We only want to call these from within 1169 | // libtwirc_handle_event() and, in one case, twirc_tick(), to avoid 1170 | // situations where they might be raised twice. Remember: closing 1171 | // the socket here might lead to an epoll event that 'naturally' 1172 | // leads to us calling the disconnect handlers anyway. If that does 1173 | // not happen, well, then so be it. If the user called upon this 1174 | // function, they should expect the connection to be down shortly 1175 | // after. Of course, this would leave us in an inconsistent state, 1176 | // as s->state would report that we're still connected, but oh well. 1177 | // TODO test/investigate further if this could be an issue or not. 1178 | } 1179 | 1180 | /* 1181 | * Sets all callback members to the dummy callback 1182 | */ 1183 | void 1184 | twirc_init_callbacks(twirc_callbacks_t *cbs) 1185 | { 1186 | // TODO figure out if there is a more elegant and dynamic way... 1187 | cbs->connect = libtwirc_on_null; 1188 | cbs->welcome = libtwirc_on_null; 1189 | cbs->globaluserstate = libtwirc_on_null; 1190 | cbs->capack = libtwirc_on_null; 1191 | cbs->ping = libtwirc_on_null; 1192 | cbs->join = libtwirc_on_null; 1193 | cbs->part = libtwirc_on_null; 1194 | cbs->mode = libtwirc_on_null; 1195 | cbs->names = libtwirc_on_null; 1196 | cbs->privmsg = libtwirc_on_null; 1197 | cbs->whisper = libtwirc_on_null; 1198 | cbs->action = libtwirc_on_null; 1199 | cbs->notice = libtwirc_on_null; 1200 | cbs->roomstate = libtwirc_on_null; 1201 | cbs->usernotice = libtwirc_on_null; 1202 | cbs->userstate = libtwirc_on_null; 1203 | cbs->clearchat = libtwirc_on_null; 1204 | cbs->clearmsg = libtwirc_on_null; 1205 | cbs->hosttarget = libtwirc_on_null; 1206 | cbs->reconnect = libtwirc_on_null; 1207 | cbs->disconnect = libtwirc_on_null; 1208 | cbs->invalidcmd = libtwirc_on_null; 1209 | cbs->other = libtwirc_on_null; 1210 | cbs->outbound = libtwirc_on_null; 1211 | } 1212 | 1213 | /* 1214 | * Returns a pointer to the state's twirc_callbacks structure. 1215 | * This allows the user to set select callbacks to their handler functions. 1216 | * Under no circumstances should the user set any callback to NULL, as this 1217 | * will eventually lead to a segmentation fault, as libtwirc relies on the 1218 | * fact that every callback that wasn't assigned to by the user is assigned 1219 | * to an internal dummy (null) event handler. 1220 | */ 1221 | twirc_callbacks_t* 1222 | twirc_get_callbacks(twirc_state_t *s) 1223 | { 1224 | return &s->cbs; 1225 | } 1226 | 1227 | /* 1228 | * Returns a pointer to a twirc_state struct, which represents the state of 1229 | * the connection to the server, the state of the user, holds the login data, 1230 | * all callback function pointers for event handling and much more. Returns 1231 | * a NULL pointer if any errors occur during initialization. 1232 | */ 1233 | twirc_state_t* 1234 | twirc_init() 1235 | { 1236 | // Seed the random number generator 1237 | srand(time(NULL)); 1238 | 1239 | // Init state struct 1240 | twirc_state_t *s = malloc(sizeof(twirc_state_t)); 1241 | if (s == NULL) { return NULL; } 1242 | memset(s, 0, sizeof(twirc_state_t)); 1243 | 1244 | // Set some defaults / initial values 1245 | s->status = TWIRC_STATUS_DISCONNECTED; 1246 | s->ip_type = TWIRC_IPV4; 1247 | s->socket_fd = -1; 1248 | s->error = 0; 1249 | 1250 | // Initialize the buffer - it will be twice the message size so it can 1251 | // easily hold an incomplete message in addition to a complete one 1252 | s->buffer = malloc(2 * TWIRC_MESSAGE_SIZE * sizeof(char)); 1253 | if (s->buffer == NULL) { return NULL; } 1254 | s->buffer[0] = '\0'; 1255 | 1256 | // Make sure the structs within state are zero-initialized 1257 | memset(&s->login, 0, sizeof(twirc_login_t)); 1258 | memset(&s->cbs, 0, sizeof(twirc_callbacks_t)); 1259 | 1260 | // Set all callbacks to the dummy callback 1261 | twirc_init_callbacks(&s->cbs); 1262 | 1263 | // All done 1264 | return s; 1265 | } 1266 | 1267 | /* 1268 | * Frees the twirc_state and all of its members. 1269 | */ 1270 | void 1271 | twirc_free(twirc_state_t *s) 1272 | { 1273 | close(s->epfd); 1274 | libtwirc_free_callbacks(s); 1275 | libtwirc_free_login(s); 1276 | free(s->buffer); 1277 | free(s); 1278 | s = NULL; 1279 | } 1280 | 1281 | /* 1282 | * Schwarzeneggers the connection with the server and frees the twirc_state. 1283 | * Hence, do not call twirc_free() after this function, it's already done. 1284 | */ 1285 | void 1286 | twirc_kill(twirc_state_t *s) 1287 | { 1288 | if (twirc_is_connected(s)) 1289 | { 1290 | twirc_disconnect(s); 1291 | } 1292 | twirc_free(s); 1293 | } 1294 | 1295 | /* 1296 | * Waits timeout milliseconds for events to happen on the IRC connection. 1297 | * Returns 0 if all events have been handled and -1 if an error has been 1298 | * encountered and/or the connection has been lost. To determine whether the 1299 | * connection has been lost, use twirc_is_connected(). If the connection is 1300 | * still up, for example, if we were interrupted by a SIGSTOP signal, then it 1301 | * is up to the user to decide whether they want to disconnect now or keep the 1302 | * connection alive. Remember, however, that messages will keep piling up in 1303 | * the kernel; if your program is handling very busy channels, you might not 1304 | * want to stay connected without handling those messages for too long. 1305 | */ 1306 | int 1307 | twirc_tick(twirc_state_t *s, int timeout) 1308 | { 1309 | struct epoll_event epev; 1310 | 1311 | // epoll_wait()/epoll_pwait() will return -1 if a signal is caught. 1312 | // User code might catch "harmless" signals, like SIGWINCH, that are 1313 | // ignored by default. This would then cause epoll_wait() to return 1314 | // with -1, hence our main loop to come to a halt. This is not what 1315 | // a user would expect; we should only come to a halt on "serious" 1316 | // signals that would cause program termination/halt by default. 1317 | // In order to achieve this, we tell epoll_pwait() to block all of 1318 | // the signals that are ignored by default. For a list of signals: 1319 | // https://en.wikipedia.org/wiki/Signal_(IPC) 1320 | 1321 | sigset_t sigset; 1322 | sigemptyset(&sigset); 1323 | sigaddset(&sigset, SIGCHLD); // default: ignore 1324 | sigaddset(&sigset, SIGCONT); // default: continue execution 1325 | sigaddset(&sigset, SIGURG); // default: ignore 1326 | sigaddset(&sigset, SIGWINCH); // default: ignore 1327 | 1328 | int num_events = epoll_pwait(s->epfd, &epev, 1, timeout, &sigset); 1329 | 1330 | // An error has occured 1331 | if (num_events == -1) 1332 | { 1333 | // The exact reason why epoll_wait failed can be queried through 1334 | // errno; the possibilities include wrong/faulty parameters and, 1335 | // more interesting, that a signal has interrupted epoll_wait(). 1336 | // Wrong parameters will either happen on the very first call or 1337 | // not at all, but a signal could come in anytime. Either way, 1338 | // epoll_wait() failing doesn't necessarily mean that we lost 1339 | // the connection with the server. Some signals, like SIGSTOP 1340 | // can mean that we're simply supposed to stop execution until 1341 | // a SIGCONT is received. Hence, it seems like a good idea to 1342 | // leave it up to the user what to do, which means that we are 1343 | // not going to quit/disconnect from IRC; we're simply going to 1344 | // return -1 to indicate an issue. The user can then check the 1345 | // connection status and decide if they want to explicitly end 1346 | // the connection or keep it alive. One exception: if we can 1347 | // actually determine, right here, that the connection seems to 1348 | // be down, then we'll set off the disconnect event handlers. 1349 | // For this, we'll use tcpsock_status(). 1350 | 1351 | // Set the error accordingly: 1352 | // - TWIRC_ERR_EPOLL_SIG if epoll_pwait() caught a signal 1353 | // - TWIRC_ERR_EPOLL_WAIT for any other error in epoll_wait() 1354 | s->error = errno == EINTR ? TWIRC_ERR_EPOLL_SIG : TWIRC_ERR_EPOLL_WAIT; 1355 | 1356 | // Were we connected previously but now seem to be disconnected? 1357 | if (twirc_is_connected(s) && tcpsock_status(s->socket_fd) == -1) 1358 | { 1359 | // ...if so, call the disconnect event handlers 1360 | libtwirc_on_disconnect(s); 1361 | s->cbs.disconnect(s, NULL); 1362 | } 1363 | return -1; 1364 | } 1365 | 1366 | // No events have occured 1367 | if (num_events == 0) 1368 | { 1369 | return 0; 1370 | } 1371 | 1372 | return libtwirc_handle_event(s, &epev); 1373 | } 1374 | 1375 | /* 1376 | * Runs an endless loop that waits for and processes IRC events until either 1377 | * the connection has been closed or some serious error has occured that caused 1378 | * twirc_tick() to return -1, at which point the loop ends. Returns 0 if the 1379 | * connection to the IRC server has been lost or 1 if the connection is still 1380 | * up and the loop has ended for some other reason (check the state's error 1381 | * field and possibly errno for additional details). 1382 | */ 1383 | int 1384 | twirc_loop(twirc_state_t *s) 1385 | { 1386 | // TODO we should probably put some connection time-out code in place 1387 | // so that we stop running after the connection attempt has been going 1388 | // on for so-and-so long. Or shall we leave that up to the user code? 1389 | 1390 | while (twirc_tick(s, -1) == 0) 1391 | { 1392 | // Nothing to do here, actually. :-) 1393 | } 1394 | return twirc_is_connected(s); 1395 | } 1396 | 1397 | --------------------------------------------------------------------------------