all repos — honk @ 6c37a51a473e364f7a84491efe095c653d8f6e2c

my fork of honk

masto.go (view raw)

  1package main
  2
  3import (
  4	"database/sql"
  5	"fmt"
  6	"log"
  7	"net/http"
  8	"slices"
  9	"strings"
 10	"time"
 11
 12	"humungus.tedunangst.com/r/webs/junk"
 13	"humungus.tedunangst.com/r/webs/login"
 14)
 15
 16type MastoApp struct {
 17	Name         string `db:"clientname"`
 18	RedirectURI  string `db:"redirecturis"`
 19	ClientID     string `db:"clientid"`
 20	ClientSecret string `db:"clientsecret"`
 21	VapidKey     string `db:"vapidkey"`
 22	AuthToken    string `db:"authtoken"`
 23	Scopes       string `db:"scopes"`
 24}
 25
 26func showoauthlogin(rw http.ResponseWriter, r *http.Request) {
 27	templinfo := make(map[string]interface{})
 28	templinfo = getInfo(r)
 29	templinfo["ClientID"] = r.URL.Query().Get("client_id")
 30	templinfo["RedirectURI"] = r.URL.Query().Get("redirect_uri")
 31
 32	if err := readviews.Execute(rw, "oauthlogin.html", templinfo); err != nil {
 33		elog.Println(err)
 34	}
 35}
 36
 37// https://docs.joinmastodon.org/methods/apps/#create
 38func apiapps(rw http.ResponseWriter, r *http.Request) {
 39	if err := r.ParseForm(); err != nil {
 40		http.Error(rw, "invalid input", http.StatusUnprocessableEntity)
 41		elog.Println(err)
 42		return
 43	}
 44	clientName := r.Form.Get("client_name")
 45	redirectURI := r.Form.Get("redirect_uris")
 46	scopes := r.Form.Get("scopes")
 47	website := r.Form.Get("website")
 48	clientID := tokengen()
 49	clientSecret := tokengen()
 50	vapidKey := tokengen()
 51
 52	_, err := stmtSaveMastoApp.Exec(clientName, redirectURI, scopes, clientID, clientSecret, vapidKey, "")
 53	if err != nil {
 54		elog.Printf("error saving masto app: %v", err)
 55		http.Error(rw, "error saving masto app", http.StatusUnprocessableEntity)
 56		return
 57	}
 58
 59	j := junk.New()
 60	j["id"] = "19"
 61	j["website"] = website
 62	j["name"] = clientName
 63	j["redirect_uri"] = redirectURI
 64	j["client_id"] = clientID
 65	j["client_secret"] = clientSecret
 66	j["vapid_key"] = vapidKey
 67
 68	fmt.Println(j.ToString())
 69	goodjunk(rw, j)
 70}
 71
 72// https://docs.joinmastodon.org/methods/oauth/#authorize
 73func oauthorize(rw http.ResponseWriter, r *http.Request) {
 74	clientID := r.FormValue("client_id")
 75	redirectURI := r.FormValue("redirect_uri")
 76
 77	if !checkClientID(clientID) {
 78		elog.Println("oauth: no such client:", clientID)
 79		rw.WriteHeader(http.StatusUnauthorized)
 80		return
 81	}
 82
 83	var nrw NotResponseWriter
 84	login.LoginFunc(&nrw, r)
 85
 86	_, err := stmtSaveMastoAppToken.Exec(nrw.auth)
 87	if err != nil {
 88		elog.Println("oauth: failed to save masto app token", err)
 89		rw.WriteHeader(http.StatusInternalServerError)
 90		return
 91	}
 92
 93	uri := fmt.Sprintf("%s?code=%s", redirectURI, nrw.auth)
 94
 95	log.Println("redirecting to", uri)
 96	rw.Header().Set("Content-Type", "")
 97	rw.Header().Set("Location", uri)
 98	rw.WriteHeader(302)
 99}
100
101// https://docs.joinmastodon.org/methods/oauth/#token
102func oauthtoken(rw http.ResponseWriter, r *http.Request) {
103	grantType := r.FormValue("grant_type")
104	code := r.FormValue("code")
105	clientID := r.FormValue("client_id")
106	clientSecret := r.FormValue("client_secret")
107	redirectURI := r.FormValue("redirect_uri")
108	gotScope := r.FormValue("scope")
109
110	if !checkClient(clientID, clientSecret) {
111		elog.Println("oauth: no such client:", clientID)
112		rw.WriteHeader(http.StatusBadRequest)
113		return
114	}
115
116	app := MastoApp{}
117	row := stmtGetMastoApp.QueryRowx(clientID)
118	err := row.StructScan(&app)
119	if err == sql.ErrNoRows {
120		elog.Printf("oauth: invalid client: %s\n", clientID)
121		rw.WriteHeader(http.StatusBadRequest)
122		return
123	}
124	log.Printf("%#v", app)
125
126	possibleScopes := strings.Split(app.Scopes, " ")
127	requestedScopes := strings.Split(gotScope, " ")
128	for _, scope := range requestedScopes {
129		if !slices.Contains(possibleScopes, scope) {
130			elog.Printf("oauth: invalid scope: %s", scope)
131			rw.WriteHeader(http.StatusBadRequest)
132			return
133		}
134	}
135
136	if app.Scopes != gotScope {
137		elog.Printf("oauth: bad scopes: got %s; want %s", gotScope, app.Scopes)
138		rw.WriteHeader(http.StatusBadRequest)
139		return
140	}
141
142	if app.RedirectURI != redirectURI {
143		elog.Println("oauth: incorrect redirect URI")
144		rw.WriteHeader(http.StatusBadRequest)
145		return
146	}
147
148	if grantType == "authorization_code" {
149		// idk if this is ok? should code be reset?
150		if app.AuthToken == code {
151			accessToken := tokengen()
152			_, err := stmtSaveMastoAccessToken.Exec(app.ClientID, accessToken)
153			if err != nil {
154				elog.Println("oauth: failed to save masto access token", err)
155				rw.WriteHeader(http.StatusInternalServerError)
156				return
157			}
158			j := junk.New()
159			j["access_token"] = accessToken
160			j["token_type"] = "Bearer"
161			j["scope"] = app.Scopes
162			j["created_at"] = time.Now().UTC().Unix()
163			goodjunk(rw, j)
164		}
165	} else {
166		// gaslight the client
167		elog.Println("oauth: bad grant_type: must be authorization_code")
168		rw.WriteHeader(http.StatusBadRequest)
169		return
170	}
171}
172
173// https://docs.joinmastodon.org/methods/instance/#v2
174func instance(rw http.ResponseWriter, r *http.Request) {
175	j := junk.New()
176
177	var servername string
178	if err := getconfig("servername", &servername); err != nil {
179		http.Error(rw, "getting servername", http.StatusInternalServerError)
180		return
181	}
182
183	j["uri"] = servername
184	j["title"] = "honk"
185	j["description"] = "federated honk conveyance"
186	j["version"] = "develop"
187
188	thumbnail := junk.New()
189	thumbnail["url"] = fmt.Sprintf("https://%s/icon.png", servername)
190	j["thumbnail"] = thumbnail
191	j["languages"] = []string{"en"}
192
193	config := junk.New()
194
195	a := junk.New()
196	a["max_featured_tags"] = 10
197	config["accounts"] = a
198
199	s := junk.New()
200	s["max_characters"] = 5000
201	s["max_media_attachments"] = 1
202	s["characters_reserved_per_url"] = 23
203	config["statuses"] = s
204
205	m := junk.New()
206	m["supported_mime_types"] = []string{
207		"image/jpeg",
208		"image/png",
209		"image/gif",
210		"image/heic",
211		"image/heif",
212		"image/webp",
213		"image/avif",
214		"video/webm",
215		"video/mp4",
216		"video/quicktime",
217		"video/ogg",
218		"audio/wave",
219		"audio/wav",
220		"audio/x-wav",
221		"audio/x-pn-wave",
222		"audio/vnd.wave",
223		"audio/ogg",
224		"audio/vorbis",
225		"audio/mpeg",
226		"audio/mp3",
227		"audio/webm",
228		"audio/flac",
229		"audio/aac",
230		"audio/m4a",
231		"audio/x-m4a",
232		"audio/mp4",
233		"audio/3gpp",
234		"video/x-ms-asf",
235	}
236
237	m["image_size_limit"] = 10485760
238	m["image_matrix_limit"] = 16777216
239	m["video_size_limit"] = 41943040
240	m["video_frame_rate_limit"] = 60
241	m["video_matrix_limit"] = 2304000
242	j["media_attachments"] = m
243
244	goodjunk(rw, j)
245
246	return
247}
248
249//    /// The ID of the account.
250//    public let id: String
251//    /// The username of the account.
252//    public let username: String
253//    /// Equals username for local users, includes @domain for remote ones.
254//    public let acct: String
255//    /// The account's display name.
256//    public let displayName: String
257//    /// Biography of user.
258//    public let note: String
259//    /// URL of the user's profile page (can be remote).
260//    public let url: String
261//    /// URL to the avatar image.
262//    public let avatar: String
263//    /// URL to the avatar static image
264//    public let avatarStatic: String
265//    /// URL to the header image.
266//    public let header: String
267//    /// URL to the header static image
268//    public let headerStatic: String
269//    /// Boolean for when the account cannot be followed without waiting for approval first.
270//    public let locked: Bool
271//    /// The time the account was created.
272//    public let createdAt: String?
273//    /// The number of followers for the account.
274//    public var followersCount: Int
275//    /// The number of accounts the given account is following.
276//    public var followingCount: Int
277//    /// The number of statuses the account has made.
278//    public let statusesCount: Int
279//    /// An array of Emoji.
280//    public let emojis: [Emoji]
281//
282//    public let fields: [HashType]
283//
284//    /// A 3-5 word description of the account
285//    /// Optional, as this is a Mammoth addition
286//    public let summary: String?
287//
288//    public let bot: Bool
289//    public let lastStatusAt: String?
290//    public let discoverable: Bool?
291//
292//    public let source: AccountSource?
293
294// https://docs.joinmastodon.org/methods/accounts/#verify_credentials
295func verifycreds(rw http.ResponseWriter, r *http.Request) {
296	user, ok := somenumberedusers.Get(UserID(1))
297	if !ok {
298		elog.Fatalf("masto: no user number 1???")
299	}
300	j := junk.New()
301	j["id"] = fmt.Sprintf("%d", user.ID)
302	j["username"] = user.Name
303	j["created_at"] = ""
304	j["acct"] = user.Name
305	j["display_name"] = user.Options.CustomDisplay
306	j["url"] = user.URL
307	j["note"] = user.About
308	j["avatar"] = user.Options.Avatar
309	j["avatar_static"] = user.Options.Avatar
310	j["header"] = user.Options.Banner
311	j["header_static"] = user.Options.Banner
312	j["followers_count"] = 0
313	j["following_count"] = 0
314	j["statuses_count"] = getlocalhonkcount()
315	j["locked"] = false
316	j["bot"] = false
317	j["last_status_at"] = ""
318	j["discoverable"] = false
319	j["source"] = nil
320	j["emojis"] = []junk.Junk{}
321	j["fields"] = []junk.Junk{}
322
323	log.Println(j.ToString())
324
325	goodjunk(rw, j)
326}